lootforge 0.3.0
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/CHANGELOG.md +87 -0
- package/README.md +764 -0
- package/bin/lootforge.js +28 -0
- package/dist/benchmarks/coarseToFineCost.d.ts +21 -0
- package/dist/benchmarks/coarseToFineCost.js +49 -0
- package/dist/benchmarks/coarseToFineCost.js.map +1 -0
- package/dist/checks/boundaryMetrics.d.ts +12 -0
- package/dist/checks/boundaryMetrics.js +102 -0
- package/dist/checks/boundaryMetrics.js.map +1 -0
- package/dist/checks/candidateScore.d.ts +11 -0
- package/dist/checks/candidateScore.js +462 -0
- package/dist/checks/candidateScore.js.map +1 -0
- package/dist/checks/commandParser.d.ts +5 -0
- package/dist/checks/commandParser.js +99 -0
- package/dist/checks/commandParser.js.map +1 -0
- package/dist/checks/consistencyOutliers.d.ts +42 -0
- package/dist/checks/consistencyOutliers.js +156 -0
- package/dist/checks/consistencyOutliers.js.map +1 -0
- package/dist/checks/imageAcceptance.d.ts +67 -0
- package/dist/checks/imageAcceptance.js +967 -0
- package/dist/checks/imageAcceptance.js.map +1 -0
- package/dist/checks/packInvariants.d.ts +56 -0
- package/dist/checks/packInvariants.js +1064 -0
- package/dist/checks/packInvariants.js.map +1 -0
- package/dist/checks/softAdapters.d.ts +25 -0
- package/dist/checks/softAdapters.js +275 -0
- package/dist/checks/softAdapters.js.map +1 -0
- package/dist/checks/vlmGate.d.ts +8 -0
- package/dist/checks/vlmGate.js +200 -0
- package/dist/checks/vlmGate.js.map +1 -0
- package/dist/cli/commands/atlas.d.ts +5 -0
- package/dist/cli/commands/atlas.js +18 -0
- package/dist/cli/commands/atlas.js.map +1 -0
- package/dist/cli/commands/eval.d.ts +6 -0
- package/dist/cli/commands/eval.js +23 -0
- package/dist/cli/commands/eval.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +18 -0
- package/dist/cli/commands/generate.js +66 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/init.d.ts +15 -0
- package/dist/cli/commands/init.js +146 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/package.d.ts +6 -0
- package/dist/cli/commands/package.js +27 -0
- package/dist/cli/commands/package.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +16 -0
- package/dist/cli/commands/plan.js +49 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/process.d.ts +14 -0
- package/dist/cli/commands/process.js +29 -0
- package/dist/cli/commands/process.js.map +1 -0
- package/dist/cli/commands/regenerate.d.ts +29 -0
- package/dist/cli/commands/regenerate.js +244 -0
- package/dist/cli/commands/regenerate.js.map +1 -0
- package/dist/cli/commands/review.d.ts +5 -0
- package/dist/cli/commands/review.js +18 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/commands/select.d.ts +6 -0
- package/dist/cli/commands/select.js +21 -0
- package/dist/cli/commands/select.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +16 -0
- package/dist/cli/commands/serve.js +100 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +17 -0
- package/dist/cli/commands/validate.js +108 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +157 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +3 -0
- package/dist/cli/parseArgs.js +37 -0
- package/dist/cli/parseArgs.js.map +1 -0
- package/dist/contracts/stageArtifacts.d.ts +4031 -0
- package/dist/contracts/stageArtifacts.js +663 -0
- package/dist/contracts/stageArtifacts.js.map +1 -0
- package/dist/manifest/load.d.ts +3 -0
- package/dist/manifest/load.js +50 -0
- package/dist/manifest/load.js.map +1 -0
- package/dist/manifest/normalize-palette.d.ts +17 -0
- package/dist/manifest/normalize-palette.js +235 -0
- package/dist/manifest/normalize-palette.js.map +1 -0
- package/dist/manifest/normalize-policy.d.ts +48 -0
- package/dist/manifest/normalize-policy.js +239 -0
- package/dist/manifest/normalize-policy.js.map +1 -0
- package/dist/manifest/normalize-prompt.d.ts +14 -0
- package/dist/manifest/normalize-prompt.js +73 -0
- package/dist/manifest/normalize-prompt.js.map +1 -0
- package/dist/manifest/normalize-target.d.ts +49 -0
- package/dist/manifest/normalize-target.js +542 -0
- package/dist/manifest/normalize-target.js.map +1 -0
- package/dist/manifest/schema.d.ts +7570 -0
- package/dist/manifest/schema.js +373 -0
- package/dist/manifest/schema.js.map +1 -0
- package/dist/manifest/semantic-validation.d.ts +4 -0
- package/dist/manifest/semantic-validation.js +526 -0
- package/dist/manifest/semantic-validation.js.map +1 -0
- package/dist/manifest/types.d.ts +263 -0
- package/dist/manifest/types.js +2 -0
- package/dist/manifest/types.js.map +1 -0
- package/dist/manifest/validate.d.ts +12 -0
- package/dist/manifest/validate.js +221 -0
- package/dist/manifest/validate.js.map +1 -0
- package/dist/output/assetPackManifest.d.ts +19 -0
- package/dist/output/assetPackManifest.js +20 -0
- package/dist/output/assetPackManifest.js.map +1 -0
- package/dist/output/catalog.d.ts +60 -0
- package/dist/output/catalog.js +107 -0
- package/dist/output/catalog.js.map +1 -0
- package/dist/output/contactSheet.d.ts +13 -0
- package/dist/output/contactSheet.js +124 -0
- package/dist/output/contactSheet.js.map +1 -0
- package/dist/output/phaserManifest.d.ts +8 -0
- package/dist/output/phaserManifest.js +25 -0
- package/dist/output/phaserManifest.js.map +1 -0
- package/dist/output/pixiManifest.d.ts +8 -0
- package/dist/output/pixiManifest.js +37 -0
- package/dist/output/pixiManifest.js.map +1 -0
- package/dist/output/provenance.d.ts +121 -0
- package/dist/output/provenance.js +10 -0
- package/dist/output/provenance.js.map +1 -0
- package/dist/output/runtimeManifests.d.ts +21 -0
- package/dist/output/runtimeManifests.js +82 -0
- package/dist/output/runtimeManifests.js.map +1 -0
- package/dist/output/unityImportManifest.d.ts +10 -0
- package/dist/output/unityImportManifest.js +58 -0
- package/dist/output/unityImportManifest.js.map +1 -0
- package/dist/output/zip.d.ts +5 -0
- package/dist/output/zip.js +68 -0
- package/dist/output/zip.js.map +1 -0
- package/dist/pipeline/atlas.d.ts +33 -0
- package/dist/pipeline/atlas.js +286 -0
- package/dist/pipeline/atlas.js.map +1 -0
- package/dist/pipeline/eval.d.ts +104 -0
- package/dist/pipeline/eval.js +246 -0
- package/dist/pipeline/eval.js.map +1 -0
- package/dist/pipeline/generate.d.ts +44 -0
- package/dist/pipeline/generate.js +1088 -0
- package/dist/pipeline/generate.js.map +1 -0
- package/dist/pipeline/package.d.ts +18 -0
- package/dist/pipeline/package.js +218 -0
- package/dist/pipeline/package.js.map +1 -0
- package/dist/pipeline/process.d.ts +15 -0
- package/dist/pipeline/process.js +776 -0
- package/dist/pipeline/process.js.map +1 -0
- package/dist/pipeline/review.d.ts +10 -0
- package/dist/pipeline/review.js +341 -0
- package/dist/pipeline/review.js.map +1 -0
- package/dist/pipeline/seamHeal.d.ts +2 -0
- package/dist/pipeline/seamHeal.js +70 -0
- package/dist/pipeline/seamHeal.js.map +1 -0
- package/dist/pipeline/select.d.ts +39 -0
- package/dist/pipeline/select.js +79 -0
- package/dist/pipeline/select.js.map +1 -0
- package/dist/providers/job.d.ts +29 -0
- package/dist/providers/job.js +113 -0
- package/dist/providers/job.js.map +1 -0
- package/dist/providers/localDiffusion.d.ts +28 -0
- package/dist/providers/localDiffusion.js +235 -0
- package/dist/providers/localDiffusion.js.map +1 -0
- package/dist/providers/nano.d.ts +36 -0
- package/dist/providers/nano.js +402 -0
- package/dist/providers/nano.js.map +1 -0
- package/dist/providers/openai.d.ts +37 -0
- package/dist/providers/openai.js +378 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/policy.d.ts +9 -0
- package/dist/providers/policy.js +192 -0
- package/dist/providers/policy.js.map +1 -0
- package/dist/providers/prompt.d.ts +3 -0
- package/dist/providers/prompt.js +63 -0
- package/dist/providers/prompt.js.map +1 -0
- package/dist/providers/registry.d.ts +24 -0
- package/dist/providers/registry.js +92 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/runtime.d.ts +15 -0
- package/dist/providers/runtime.js +101 -0
- package/dist/providers/runtime.js.map +1 -0
- package/dist/providers/runtimeConfig.d.ts +20 -0
- package/dist/providers/runtimeConfig.js +146 -0
- package/dist/providers/runtimeConfig.js.map +1 -0
- package/dist/providers/types-core.d.ts +514 -0
- package/dist/providers/types-core.js +60 -0
- package/dist/providers/types-core.js.map +1 -0
- package/dist/providers/types.d.ts +4 -0
- package/dist/providers/types.js +5 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/service/generationRequest.d.ts +58 -0
- package/dist/service/generationRequest.js +203 -0
- package/dist/service/generationRequest.js.map +1 -0
- package/dist/service/providerCapabilities.d.ts +40 -0
- package/dist/service/providerCapabilities.js +114 -0
- package/dist/service/providerCapabilities.js.map +1 -0
- package/dist/service/server.d.ts +31 -0
- package/dist/service/server.js +774 -0
- package/dist/service/server.js.map +1 -0
- package/dist/shared/errors.d.ts +13 -0
- package/dist/shared/errors.js +24 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/fs.d.ts +6 -0
- package/dist/shared/fs.js +30 -0
- package/dist/shared/fs.js.map +1 -0
- package/dist/shared/image.d.ts +25 -0
- package/dist/shared/image.js +136 -0
- package/dist/shared/image.js.map +1 -0
- package/dist/shared/paths.d.ts +30 -0
- package/dist/shared/paths.js +103 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/schemas.d.ts +209 -0
- package/dist/shared/schemas.js +93 -0
- package/dist/shared/schemas.js.map +1 -0
- package/dist/shared/typeGuards.d.ts +1 -0
- package/dist/shared/typeGuards.js +4 -0
- package/dist/shared/typeGuards.js.map +1 -0
- package/dist/shared/zod.d.ts +1 -0
- package/dist/shared/zod.js +14 -0
- package/dist/shared/zod.js.map +1 -0
- package/dist/showcase/format.d.ts +9 -0
- package/dist/showcase/format.js +61 -0
- package/dist/showcase/format.js.map +1 -0
- package/dist/showcase/panelRenderer.d.ts +59 -0
- package/dist/showcase/panelRenderer.js +294 -0
- package/dist/showcase/panelRenderer.js.map +1 -0
- package/dist/showcase/releaseConfig.d.ts +233 -0
- package/dist/showcase/releaseConfig.js +75 -0
- package/dist/showcase/releaseConfig.js.map +1 -0
- package/dist/showcase/releaseEvidence.d.ts +25 -0
- package/dist/showcase/releaseEvidence.js +540 -0
- package/dist/showcase/releaseEvidence.js.map +1 -0
- package/dist/showcase/releaseEvidenceSchema.d.ts +1611 -0
- package/dist/showcase/releaseEvidenceSchema.js +165 -0
- package/dist/showcase/releaseEvidenceSchema.js.map +1 -0
- package/dist/showcase/scenarioRenderer.d.ts +19 -0
- package/dist/showcase/scenarioRenderer.js +488 -0
- package/dist/showcase/scenarioRenderer.js.map +1 -0
- package/docs/ADAPTER_CONTRACT.md +141 -0
- package/docs/ENGINE_TARGETING.md +86 -0
- package/docs/MANIFEST_POLICY_COVERAGE.md +130 -0
- package/docs/RELEASE_WORKFLOW.md +117 -0
- package/docs/ROADMAP.md +411 -0
- package/docs/ROADMAP_ISSUES.md +244 -0
- package/docs/SERVICE_MODE.md +137 -0
- package/docs/manifest-schema.md +254 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1088 @@
|
|
|
1
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
2
|
+
import { access, cp, mkdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import sharp from "sharp";
|
|
5
|
+
import { scoreCandidateImages, } from "../checks/candidateScore.js";
|
|
6
|
+
import { writeRunProvenance } from "../output/provenance.js";
|
|
7
|
+
import { createProviderRegistry, getProvider, resolveTargetProviderRoute, } from "../providers/registry.js";
|
|
8
|
+
import { resolveProviderRegistryOptions } from "../providers/runtimeConfig.js";
|
|
9
|
+
import { getTargetGenerationPolicy, normalizeGenerationPolicyForProvider, nowIso, parseProviderSelection, ProviderError, sha256Hex, } from "../providers/types.js";
|
|
10
|
+
import { normalizeTargetOutPath, resolvePathWithinRoot, resolveStagePathLayout, } from "../shared/paths.js";
|
|
11
|
+
const COARSE_TO_FINE_DRAFT_SCORING_OPTIONS = {
|
|
12
|
+
includeSoftAdapters: false,
|
|
13
|
+
includeVlmGate: false,
|
|
14
|
+
includePaletteCheck: false,
|
|
15
|
+
};
|
|
16
|
+
const AGENTIC_RETRY_TRIGGER_REASONS = new Set([
|
|
17
|
+
"vlm_gate_below_threshold",
|
|
18
|
+
"alpha_halo_risk_exceeded",
|
|
19
|
+
"alpha_stray_noise_exceeded",
|
|
20
|
+
"alpha_edge_sharpness_too_low",
|
|
21
|
+
]);
|
|
22
|
+
export async function runGeneratePipeline(options) {
|
|
23
|
+
const layout = resolveStagePathLayout(options.outDir);
|
|
24
|
+
const targetsIndexPath = path.resolve(options.targetsIndexPath ?? path.join(layout.jobsDir, "targets-index.json"));
|
|
25
|
+
const providerSelection = options.provider ?? "auto";
|
|
26
|
+
const imagesDir = layout.rawDir;
|
|
27
|
+
const skipLocked = options.skipLocked ?? true;
|
|
28
|
+
const indexRaw = await readFile(targetsIndexPath, "utf8");
|
|
29
|
+
const index = parseTargetsIndex(indexRaw, targetsIndexPath);
|
|
30
|
+
const registry = options.registry ??
|
|
31
|
+
createProviderRegistry(await resolveProviderRegistryOptions(resolveManifestPathFromIndex(index, targetsIndexPath)));
|
|
32
|
+
const targets = normalizeTargets(index, targetsIndexPath);
|
|
33
|
+
const allTargetsById = new Map(targets.map((target) => [target.id, target]));
|
|
34
|
+
const filteredTargets = filterTargetsByIds(targets.filter((target) => target.generationDisabled !== true), options.ids);
|
|
35
|
+
const lock = await readSelectionLock(path.resolve(options.selectionLockPath ?? path.join(layout.outDir, "locks", "selection-lock.json")));
|
|
36
|
+
const lockByTargetId = new Map((lock.targets ?? []).map((item) => [item.targetId, item]));
|
|
37
|
+
const inputHash = sha256Hex(indexRaw);
|
|
38
|
+
const startedAt = nowIso(options.now);
|
|
39
|
+
const runId = options.runId ?? sha256Hex(`${inputHash}:${startedAt}`).slice(0, 16);
|
|
40
|
+
await mkdir(imagesDir, { recursive: true });
|
|
41
|
+
const existingTargetOutputIds = await collectExistingTargetOutputIds(targets, imagesDir);
|
|
42
|
+
const tasks = filteredTargets.map((target, targetIndex) => {
|
|
43
|
+
const route = resolveTargetProviderRoute(target, providerSelection);
|
|
44
|
+
return {
|
|
45
|
+
target,
|
|
46
|
+
targetIndex,
|
|
47
|
+
primaryProvider: route.primary,
|
|
48
|
+
fallbackProviders: route.fallbacks,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
const taskById = new Map(tasks.map((task) => [task.target.id, task]));
|
|
52
|
+
const executionStages = buildTaskExecutionStages(tasks, {
|
|
53
|
+
allTargetsById,
|
|
54
|
+
existingTargetOutputIds,
|
|
55
|
+
});
|
|
56
|
+
options.onProgress?.({
|
|
57
|
+
type: "prepare",
|
|
58
|
+
totalJobs: tasks.length,
|
|
59
|
+
});
|
|
60
|
+
const results = [];
|
|
61
|
+
const failures = [];
|
|
62
|
+
for (const stageTasks of executionStages) {
|
|
63
|
+
const groupedTasks = new Map();
|
|
64
|
+
for (const task of stageTasks) {
|
|
65
|
+
const existing = groupedTasks.get(task.primaryProvider);
|
|
66
|
+
if (existing) {
|
|
67
|
+
existing.push(task);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
groupedTasks.set(task.primaryProvider, [task]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
await Promise.all(Array.from(groupedTasks.entries()).map(async ([providerName, providerTasks]) => {
|
|
74
|
+
const provider = getProvider(registry, providerName);
|
|
75
|
+
const providerConcurrency = Math.max(1, ...providerTasks.map((task) => task.target.generationPolicy?.providerConcurrency ?? 0), provider.capabilities.defaultConcurrency);
|
|
76
|
+
const queue = [...providerTasks].sort((left, right) => left.target.id.localeCompare(right.target.id));
|
|
77
|
+
let nextTask = 0;
|
|
78
|
+
let nextScheduledStartAt = 0;
|
|
79
|
+
const reserveWaitMs = (task) => {
|
|
80
|
+
const minDelayMs = computeProviderDelayMs(task, provider.capabilities.minDelayMs);
|
|
81
|
+
const nowMs = Date.now();
|
|
82
|
+
const scheduledStart = Math.max(nowMs, nextScheduledStartAt);
|
|
83
|
+
nextScheduledStartAt = scheduledStart + minDelayMs;
|
|
84
|
+
return Math.max(0, scheduledStart - nowMs);
|
|
85
|
+
};
|
|
86
|
+
const workers = [];
|
|
87
|
+
for (let workerIndex = 0; workerIndex < providerConcurrency; workerIndex += 1) {
|
|
88
|
+
workers.push((async () => {
|
|
89
|
+
while (nextTask < queue.length) {
|
|
90
|
+
const currentIndex = nextTask;
|
|
91
|
+
nextTask += 1;
|
|
92
|
+
const task = queue[currentIndex];
|
|
93
|
+
const waitMs = reserveWaitMs(task);
|
|
94
|
+
if (waitMs > 0) {
|
|
95
|
+
await delay(waitMs);
|
|
96
|
+
}
|
|
97
|
+
const progressIndex = task.targetIndex;
|
|
98
|
+
options.onProgress?.({
|
|
99
|
+
type: "job_start",
|
|
100
|
+
totalJobs: tasks.length,
|
|
101
|
+
jobIndex: progressIndex,
|
|
102
|
+
targetId: task.target.id,
|
|
103
|
+
provider: task.primaryProvider,
|
|
104
|
+
model: task.target.model,
|
|
105
|
+
});
|
|
106
|
+
try {
|
|
107
|
+
const result = await runTaskWithFallback({
|
|
108
|
+
task,
|
|
109
|
+
outDir: layout.outDir,
|
|
110
|
+
imagesDir,
|
|
111
|
+
now: options.now,
|
|
112
|
+
fetchImpl: options.fetchImpl,
|
|
113
|
+
registry,
|
|
114
|
+
lockByTargetId,
|
|
115
|
+
skipLocked,
|
|
116
|
+
taskById,
|
|
117
|
+
allTargetsById,
|
|
118
|
+
});
|
|
119
|
+
results.push(result);
|
|
120
|
+
options.onProgress?.({
|
|
121
|
+
type: "job_finish",
|
|
122
|
+
totalJobs: tasks.length,
|
|
123
|
+
jobIndex: progressIndex,
|
|
124
|
+
targetId: task.target.id,
|
|
125
|
+
provider: result.provider,
|
|
126
|
+
model: result.model,
|
|
127
|
+
bytesWritten: result.bytesWritten,
|
|
128
|
+
outputPath: result.outputPath,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
failures.push({
|
|
134
|
+
targetId: task.target.id,
|
|
135
|
+
provider: task.primaryProvider,
|
|
136
|
+
attemptedProviders: [task.primaryProvider, ...task.fallbackProviders],
|
|
137
|
+
message,
|
|
138
|
+
});
|
|
139
|
+
options.onProgress?.({
|
|
140
|
+
type: "job_error",
|
|
141
|
+
totalJobs: tasks.length,
|
|
142
|
+
jobIndex: progressIndex,
|
|
143
|
+
targetId: task.target.id,
|
|
144
|
+
provider: task.primaryProvider,
|
|
145
|
+
model: task.target.model,
|
|
146
|
+
message,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
})());
|
|
151
|
+
}
|
|
152
|
+
await Promise.all(workers);
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
results.sort((left, right) => left.targetId.localeCompare(right.targetId));
|
|
156
|
+
failures.sort((left, right) => left.targetId.localeCompare(right.targetId));
|
|
157
|
+
const finishedAt = nowIso(options.now);
|
|
158
|
+
const provenancePath = await writeRunProvenance(layout.outDir, {
|
|
159
|
+
runId,
|
|
160
|
+
inputHash,
|
|
161
|
+
startedAt,
|
|
162
|
+
finishedAt,
|
|
163
|
+
generatedAt: finishedAt,
|
|
164
|
+
jobs: results.map((result) => ({
|
|
165
|
+
jobId: result.jobId,
|
|
166
|
+
provider: result.provider,
|
|
167
|
+
model: result.model,
|
|
168
|
+
targetId: result.targetId,
|
|
169
|
+
prompt: result.prompt,
|
|
170
|
+
inputHash: result.inputHash,
|
|
171
|
+
startedAt: result.startedAt,
|
|
172
|
+
finishedAt: result.finishedAt,
|
|
173
|
+
outputPath: result.outputPath,
|
|
174
|
+
bytesWritten: result.bytesWritten,
|
|
175
|
+
skipped: result.skipped,
|
|
176
|
+
candidateOutputs: result.candidateOutputs,
|
|
177
|
+
candidateScores: result.candidateScores,
|
|
178
|
+
coarseToFine: result.coarseToFine,
|
|
179
|
+
agenticRetry: result.agenticRetry,
|
|
180
|
+
styleReferenceLineage: result.styleReferenceLineage,
|
|
181
|
+
generationMode: result.generationMode,
|
|
182
|
+
edit: result.edit,
|
|
183
|
+
regenerationSource: result.regenerationSource,
|
|
184
|
+
})),
|
|
185
|
+
failures,
|
|
186
|
+
});
|
|
187
|
+
if (failures.length > 0) {
|
|
188
|
+
const firstFailure = failures[0];
|
|
189
|
+
throw new Error(`Generation failed for ${failures.length} target(s): ${failures
|
|
190
|
+
.slice(0, 5)
|
|
191
|
+
.map((failure) => failure.targetId)
|
|
192
|
+
.join(", ")}. First failure (${firstFailure.targetId}): ${firstFailure.message}`);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
runId,
|
|
196
|
+
inputHash,
|
|
197
|
+
targetsIndexPath,
|
|
198
|
+
imagesDir,
|
|
199
|
+
provenancePath,
|
|
200
|
+
jobs: results,
|
|
201
|
+
failures,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export function parseGenerateProviderFlag(value) {
|
|
205
|
+
return parseProviderSelection(value);
|
|
206
|
+
}
|
|
207
|
+
function parseTargetsIndex(raw, filePath) {
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(raw);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
throw new Error(`Failed to parse targets index JSON (${filePath}): ${error instanceof Error ? error.message : String(error)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function normalizeTargets(index, filePath) {
|
|
216
|
+
if (!Array.isArray(index.targets) || index.targets.length === 0) {
|
|
217
|
+
throw new Error(`No targets found in planned index: ${filePath}`);
|
|
218
|
+
}
|
|
219
|
+
return index.targets.map((target, targetIndex) => {
|
|
220
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
221
|
+
if (!target || typeof target !== "object") {
|
|
222
|
+
throw new Error(`Invalid target at index ${targetIndex} in ${filePath}`);
|
|
223
|
+
}
|
|
224
|
+
if (typeof target.id !== "string" || target.id.trim() === "") {
|
|
225
|
+
throw new Error(`targets[${targetIndex}].id must be a non-empty string`);
|
|
226
|
+
}
|
|
227
|
+
if (typeof target.out !== "string" || target.out.trim() === "") {
|
|
228
|
+
throw new Error(`targets[${targetIndex}].out must be a non-empty string`);
|
|
229
|
+
}
|
|
230
|
+
let normalizedOut;
|
|
231
|
+
try {
|
|
232
|
+
normalizedOut = normalizeTargetOutPath(target.out);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
throw new Error(`targets[${targetIndex}].out is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
236
|
+
}
|
|
237
|
+
if (typeof target.promptSpec.primary !== "string") {
|
|
238
|
+
throw new Error(`targets[${targetIndex}].promptSpec.primary must be a string`);
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
...target,
|
|
242
|
+
out: normalizedOut,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function filterTargetsByIds(targets, ids) {
|
|
247
|
+
if (!ids || ids.length === 0) {
|
|
248
|
+
return targets;
|
|
249
|
+
}
|
|
250
|
+
const idSet = new Set(ids.map((id) => id.trim()).filter(Boolean));
|
|
251
|
+
if (idSet.size === 0) {
|
|
252
|
+
return targets;
|
|
253
|
+
}
|
|
254
|
+
const filtered = targets.filter((target) => idSet.has(target.id));
|
|
255
|
+
if (filtered.length === 0) {
|
|
256
|
+
throw new Error(`No targets matched --ids (${Array.from(idSet).join(",")}). Check planned target ids.`);
|
|
257
|
+
}
|
|
258
|
+
return filtered;
|
|
259
|
+
}
|
|
260
|
+
function buildTaskExecutionStages(tasks, options) {
|
|
261
|
+
const tasksById = new Map(tasks.map((task) => [task.target.id, task]));
|
|
262
|
+
const inDegree = new Map();
|
|
263
|
+
const adjacency = new Map();
|
|
264
|
+
for (const task of tasks) {
|
|
265
|
+
inDegree.set(task.target.id, 0);
|
|
266
|
+
adjacency.set(task.target.id, []);
|
|
267
|
+
}
|
|
268
|
+
for (const task of tasks) {
|
|
269
|
+
const dependencies = dedupeTargetIdList(task.target.dependsOn ?? []);
|
|
270
|
+
for (const dependencyId of dependencies) {
|
|
271
|
+
if (dependencyId === task.target.id) {
|
|
272
|
+
throw new Error(`Target "${task.target.id}" cannot depend on itself.`);
|
|
273
|
+
}
|
|
274
|
+
if (!tasksById.has(dependencyId)) {
|
|
275
|
+
const dependencyTarget = options.allTargetsById.get(dependencyId);
|
|
276
|
+
if (!dependencyTarget) {
|
|
277
|
+
throw new Error(`Target "${task.target.id}" depends on "${dependencyId}", but no matching planned target exists.`);
|
|
278
|
+
}
|
|
279
|
+
if (options.existingTargetOutputIds.has(dependencyId)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
throw new Error(`Target "${task.target.id}" depends on "${dependencyId}", but that target is not in the current generation set and ${dependencyTarget.out} has not been generated yet.`);
|
|
283
|
+
}
|
|
284
|
+
adjacency.get(dependencyId)?.push(task.target.id);
|
|
285
|
+
inDegree.set(task.target.id, (inDegree.get(task.target.id) ?? 0) + 1);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
let currentStage = tasks
|
|
289
|
+
.map((task) => task.target.id)
|
|
290
|
+
.filter((targetId) => (inDegree.get(targetId) ?? 0) === 0)
|
|
291
|
+
.sort((left, right) => left.localeCompare(right));
|
|
292
|
+
const stages = [];
|
|
293
|
+
let visited = 0;
|
|
294
|
+
while (currentStage.length > 0) {
|
|
295
|
+
const stageTasks = currentStage
|
|
296
|
+
.map((targetId) => tasksById.get(targetId))
|
|
297
|
+
.filter((task) => Boolean(task));
|
|
298
|
+
stages.push(stageTasks);
|
|
299
|
+
visited += stageTasks.length;
|
|
300
|
+
const nextStageCandidates = new Set();
|
|
301
|
+
for (const targetId of currentStage) {
|
|
302
|
+
const dependents = adjacency.get(targetId) ?? [];
|
|
303
|
+
for (const dependentId of dependents) {
|
|
304
|
+
const nextDegree = (inDegree.get(dependentId) ?? 0) - 1;
|
|
305
|
+
inDegree.set(dependentId, nextDegree);
|
|
306
|
+
if (nextDegree === 0) {
|
|
307
|
+
nextStageCandidates.add(dependentId);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
currentStage = Array.from(nextStageCandidates).sort((left, right) => left.localeCompare(right));
|
|
312
|
+
}
|
|
313
|
+
if (visited !== tasks.length) {
|
|
314
|
+
const blockedTargets = tasks
|
|
315
|
+
.map((task) => task.target.id)
|
|
316
|
+
.filter((targetId) => (inDegree.get(targetId) ?? 0) > 0)
|
|
317
|
+
.sort((left, right) => left.localeCompare(right));
|
|
318
|
+
throw new Error(`Dependency cycle detected in generation targets: ${blockedTargets.join(", ")}.`);
|
|
319
|
+
}
|
|
320
|
+
return stages;
|
|
321
|
+
}
|
|
322
|
+
async function runTaskWithFallback(params) {
|
|
323
|
+
const providerChain = [params.task.primaryProvider, ...params.task.fallbackProviders];
|
|
324
|
+
let lastError;
|
|
325
|
+
for (const providerName of providerChain) {
|
|
326
|
+
const provider = getProvider(params.registry, providerName);
|
|
327
|
+
const normalizedPolicy = resolveProviderGenerationPolicy(params.task.target, providerName);
|
|
328
|
+
const draftTarget = materializeDraftTarget(params.task.target, normalizedPolicy);
|
|
329
|
+
if (targetRequiresEditSupport(params.task.target) && !provider.supports("image-edits")) {
|
|
330
|
+
lastError = new Error(`Provider "${providerName}" does not support edit-first generation for target "${params.task.target.id}".`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// Resolve styleReferenceFrom to actual file paths and inject them into
|
|
334
|
+
// the target's styleReferenceImages so providers can send them as visual
|
|
335
|
+
// references (Nano sends inline data, OpenAI uses the edit endpoint).
|
|
336
|
+
const styleReferenceLineage = await resolveStyleReferenceLineage({
|
|
337
|
+
task: params.task,
|
|
338
|
+
taskById: params.taskById,
|
|
339
|
+
allTargetsById: params.allTargetsById,
|
|
340
|
+
imagesDir: params.imagesDir,
|
|
341
|
+
});
|
|
342
|
+
const resolvedStyleRefPaths = (styleReferenceLineage ?? []).flatMap((entry) => entry.source === "target-output" && entry.resolvedPath ? [entry.resolvedPath] : []);
|
|
343
|
+
const enrichedTarget = resolvedStyleRefPaths.length > 0
|
|
344
|
+
? {
|
|
345
|
+
...draftTarget,
|
|
346
|
+
styleReferenceImages: [
|
|
347
|
+
...(draftTarget.styleReferenceImages ?? []),
|
|
348
|
+
...resolvedStyleRefPaths,
|
|
349
|
+
],
|
|
350
|
+
}
|
|
351
|
+
: draftTarget;
|
|
352
|
+
const preparedJobs = await provider.prepareJobs([enrichedTarget], {
|
|
353
|
+
outDir: params.outDir,
|
|
354
|
+
imagesDir: params.imagesDir,
|
|
355
|
+
now: params.now,
|
|
356
|
+
});
|
|
357
|
+
if (preparedJobs.length === 0) {
|
|
358
|
+
lastError = new Error(`Provider ${providerName} produced no jobs for ${params.task.target.id}`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const job = preparedJobs[0];
|
|
362
|
+
const lockEntry = params.lockByTargetId.get(params.task.target.id);
|
|
363
|
+
if (params.skipLocked && lockEntry?.approved && lockEntry.inputHash === job.inputHash) {
|
|
364
|
+
let lockedPath;
|
|
365
|
+
try {
|
|
366
|
+
lockedPath = resolvePathWithinRoot(params.outDir, lockEntry.selectedOutputPath, `selection lock output path for target "${job.targetId}"`);
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
throw new Error(`Selection lock output path for "${job.targetId}" must stay within --out (${params.outDir}).`, { cause: error });
|
|
370
|
+
}
|
|
371
|
+
if (await fileExists(lockedPath)) {
|
|
372
|
+
if (lockedPath !== job.outPath) {
|
|
373
|
+
await cp(lockedPath, job.outPath, { force: true });
|
|
374
|
+
}
|
|
375
|
+
const fileStat = await stat(job.outPath);
|
|
376
|
+
return {
|
|
377
|
+
jobId: job.id,
|
|
378
|
+
provider: providerName,
|
|
379
|
+
model: job.model,
|
|
380
|
+
targetId: job.targetId,
|
|
381
|
+
prompt: job.prompt,
|
|
382
|
+
outputPath: job.outPath,
|
|
383
|
+
bytesWritten: fileStat.size,
|
|
384
|
+
inputHash: job.inputHash,
|
|
385
|
+
startedAt: nowIso(params.now),
|
|
386
|
+
finishedAt: nowIso(params.now),
|
|
387
|
+
skipped: true,
|
|
388
|
+
generationMode: params.task.target.generationMode,
|
|
389
|
+
edit: params.task.target.edit,
|
|
390
|
+
regenerationSource: params.task.target.regenerationSource,
|
|
391
|
+
warnings: [`Skipped generation for ${job.targetId}; approved lock matched input hash.`],
|
|
392
|
+
...(styleReferenceLineage ? { styleReferenceLineage } : {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const attempts = Math.max(1, job.maxRetries + 1);
|
|
397
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
398
|
+
try {
|
|
399
|
+
const runResult = await provider.runJob(job, {
|
|
400
|
+
outDir: params.outDir,
|
|
401
|
+
imagesDir: params.imagesDir,
|
|
402
|
+
now: params.now,
|
|
403
|
+
fetchImpl: params.fetchImpl,
|
|
404
|
+
});
|
|
405
|
+
const coarseToFinePolicy = normalizedPolicy.coarseToFine;
|
|
406
|
+
const draftCandidates = await scoreProviderRunCandidates({
|
|
407
|
+
target: params.task.target,
|
|
408
|
+
runResult,
|
|
409
|
+
outDir: params.outDir,
|
|
410
|
+
...(coarseToFinePolicy?.enabled
|
|
411
|
+
? { scoreOptions: COARSE_TO_FINE_DRAFT_SCORING_OPTIONS }
|
|
412
|
+
: {}),
|
|
413
|
+
});
|
|
414
|
+
let candidateSet;
|
|
415
|
+
let coarseToFineRecord;
|
|
416
|
+
if (coarseToFinePolicy?.enabled) {
|
|
417
|
+
const coarseToFineResult = await runCoarseToFineRefinement({
|
|
418
|
+
provider,
|
|
419
|
+
providerName,
|
|
420
|
+
task: params.task,
|
|
421
|
+
runContext: {
|
|
422
|
+
outDir: params.outDir,
|
|
423
|
+
imagesDir: params.imagesDir,
|
|
424
|
+
now: params.now,
|
|
425
|
+
fetchImpl: params.fetchImpl,
|
|
426
|
+
},
|
|
427
|
+
policy: coarseToFinePolicy,
|
|
428
|
+
normalizedPolicy,
|
|
429
|
+
draftCandidates,
|
|
430
|
+
});
|
|
431
|
+
candidateSet = {
|
|
432
|
+
candidateOutputs: coarseToFineResult.candidateOutputs,
|
|
433
|
+
scores: coarseToFineResult.scores,
|
|
434
|
+
bestPath: coarseToFineResult.bestPath,
|
|
435
|
+
};
|
|
436
|
+
coarseToFineRecord = coarseToFineResult.coarseToFine;
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
candidateSet = draftCandidates;
|
|
440
|
+
}
|
|
441
|
+
const autoCorrection = await runAgenticAutoCorrection({
|
|
442
|
+
provider,
|
|
443
|
+
providerName,
|
|
444
|
+
task: params.task,
|
|
445
|
+
runContext: {
|
|
446
|
+
outDir: params.outDir,
|
|
447
|
+
imagesDir: params.imagesDir,
|
|
448
|
+
now: params.now,
|
|
449
|
+
fetchImpl: params.fetchImpl,
|
|
450
|
+
},
|
|
451
|
+
normalizedPolicy,
|
|
452
|
+
initialCandidates: candidateSet,
|
|
453
|
+
});
|
|
454
|
+
await copyIfDifferent(autoCorrection.bestPath, job.outPath);
|
|
455
|
+
// If this target is a strip generator (frame 0 of a batch animation),
|
|
456
|
+
// slice the wide strip image into individual frame files so downstream
|
|
457
|
+
// processing can treat them as normal per-frame outputs.
|
|
458
|
+
const strip = params.task.target.spritesheet?.stripGeneration;
|
|
459
|
+
if (strip && strip.frameCount > 1) {
|
|
460
|
+
await sliceStripIntoFrames(job.outPath, strip, params.imagesDir);
|
|
461
|
+
}
|
|
462
|
+
const fileStat = await stat(job.outPath);
|
|
463
|
+
return {
|
|
464
|
+
...runResult,
|
|
465
|
+
provider: providerName,
|
|
466
|
+
outputPath: job.outPath,
|
|
467
|
+
bytesWritten: fileStat.size,
|
|
468
|
+
candidateOutputs: autoCorrection.candidateOutputs,
|
|
469
|
+
candidateScores: autoCorrection.scores,
|
|
470
|
+
...(coarseToFineRecord ? { coarseToFine: coarseToFineRecord } : {}),
|
|
471
|
+
...(autoCorrection.agenticRetry ? { agenticRetry: autoCorrection.agenticRetry } : {}),
|
|
472
|
+
...(styleReferenceLineage ? { styleReferenceLineage } : {}),
|
|
473
|
+
generationMode: params.task.target.generationMode,
|
|
474
|
+
edit: params.task.target.edit,
|
|
475
|
+
regenerationSource: params.task.target.regenerationSource,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
lastError = provider.normalizeError(error);
|
|
480
|
+
if (attempt < attempts) {
|
|
481
|
+
await delay(backoffMsForAttempt(attempt));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
throw lastError instanceof Error
|
|
487
|
+
? lastError
|
|
488
|
+
: new Error(`Generation failed for target ${params.task.target.id}`);
|
|
489
|
+
}
|
|
490
|
+
function resolveProviderGenerationPolicy(target, providerName) {
|
|
491
|
+
return normalizeGenerationPolicyForProvider(providerName, getTargetGenerationPolicy(target))
|
|
492
|
+
.policy;
|
|
493
|
+
}
|
|
494
|
+
function materializeDraftTarget(target, normalizedPolicy) {
|
|
495
|
+
if (!normalizedPolicy.coarseToFine?.enabled || !normalizedPolicy.draftQuality) {
|
|
496
|
+
return target;
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
...target,
|
|
500
|
+
generationPolicy: {
|
|
501
|
+
...(target.generationPolicy ?? {}),
|
|
502
|
+
quality: normalizedPolicy.draftQuality,
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
async function scoreProviderRunCandidates(params) {
|
|
507
|
+
const candidateOutputs = params.runResult.candidateOutputs && params.runResult.candidateOutputs.length > 0
|
|
508
|
+
? params.runResult.candidateOutputs
|
|
509
|
+
: [
|
|
510
|
+
{
|
|
511
|
+
outputPath: params.runResult.outputPath,
|
|
512
|
+
bytesWritten: params.runResult.bytesWritten,
|
|
513
|
+
},
|
|
514
|
+
];
|
|
515
|
+
const candidateSelection = await scoreCandidateImages(params.target, candidateOutputs.map((candidate) => candidate.outputPath), {
|
|
516
|
+
outDir: params.outDir,
|
|
517
|
+
...(params.scoreOptions ?? {}),
|
|
518
|
+
});
|
|
519
|
+
return {
|
|
520
|
+
candidateOutputs,
|
|
521
|
+
scores: candidateSelection.scores,
|
|
522
|
+
bestPath: candidateSelection.bestPath,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
async function runCoarseToFineRefinement(params) {
|
|
526
|
+
const draftQuality = params.normalizedPolicy.draftQuality ?? params.normalizedPolicy.quality;
|
|
527
|
+
const finalQuality = params.normalizedPolicy.finalQuality ?? params.normalizedPolicy.quality;
|
|
528
|
+
const promotedRows = [];
|
|
529
|
+
const discardedRows = [];
|
|
530
|
+
const warnings = [];
|
|
531
|
+
const promotedSet = new Set();
|
|
532
|
+
for (const score of params.draftCandidates.scores) {
|
|
533
|
+
let discardReason = null;
|
|
534
|
+
if (params.policy.requireDraftAcceptance && !score.passedAcceptance) {
|
|
535
|
+
discardReason = "draft_failed_acceptance";
|
|
536
|
+
}
|
|
537
|
+
else if (typeof params.policy.minDraftScore === "number" &&
|
|
538
|
+
score.score < params.policy.minDraftScore) {
|
|
539
|
+
discardReason = "below_min_draft_score";
|
|
540
|
+
}
|
|
541
|
+
else if (promotedRows.length >= params.policy.promoteTopK) {
|
|
542
|
+
discardReason = "outside_top_k";
|
|
543
|
+
}
|
|
544
|
+
if (discardReason) {
|
|
545
|
+
discardedRows.push({
|
|
546
|
+
outputPath: score.outputPath,
|
|
547
|
+
score: score.score,
|
|
548
|
+
passedAcceptance: score.passedAcceptance,
|
|
549
|
+
reason: discardReason,
|
|
550
|
+
});
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
promotedRows.push({
|
|
554
|
+
outputPath: score.outputPath,
|
|
555
|
+
score: score.score,
|
|
556
|
+
passedAcceptance: score.passedAcceptance,
|
|
557
|
+
});
|
|
558
|
+
promotedSet.add(score.outputPath);
|
|
559
|
+
}
|
|
560
|
+
const draftScoresDecorated = params.draftCandidates.scores.map((score) => ({
|
|
561
|
+
...score,
|
|
562
|
+
stage: "draft",
|
|
563
|
+
promoted: promotedSet.has(score.outputPath),
|
|
564
|
+
selected: false,
|
|
565
|
+
}));
|
|
566
|
+
const coarseSummaryBase = {
|
|
567
|
+
enabled: true,
|
|
568
|
+
draftQuality,
|
|
569
|
+
finalQuality,
|
|
570
|
+
promoteTopK: params.policy.promoteTopK,
|
|
571
|
+
...(typeof params.policy.minDraftScore === "number"
|
|
572
|
+
? { minDraftScore: params.policy.minDraftScore }
|
|
573
|
+
: {}),
|
|
574
|
+
requireDraftAcceptance: params.policy.requireDraftAcceptance,
|
|
575
|
+
draftCandidateCount: params.draftCandidates.candidateOutputs.length,
|
|
576
|
+
};
|
|
577
|
+
if (promotedRows.length === 0) {
|
|
578
|
+
return {
|
|
579
|
+
bestPath: params.draftCandidates.bestPath,
|
|
580
|
+
candidateOutputs: params.draftCandidates.candidateOutputs,
|
|
581
|
+
scores: draftScoresDecorated.map((score) => ({
|
|
582
|
+
...score,
|
|
583
|
+
selected: score.outputPath === params.draftCandidates.bestPath,
|
|
584
|
+
})),
|
|
585
|
+
coarseToFine: {
|
|
586
|
+
...coarseSummaryBase,
|
|
587
|
+
promoted: promotedRows,
|
|
588
|
+
discarded: discardedRows,
|
|
589
|
+
skippedReason: "no_candidates_promoted",
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
if (!params.provider.supports("image-edits")) {
|
|
594
|
+
warnings.push(`Provider "${params.providerName}" lacks image-edits support; coarse-to-fine refinement skipped.`);
|
|
595
|
+
return {
|
|
596
|
+
bestPath: params.draftCandidates.bestPath,
|
|
597
|
+
candidateOutputs: params.draftCandidates.candidateOutputs,
|
|
598
|
+
scores: draftScoresDecorated.map((score) => ({
|
|
599
|
+
...score,
|
|
600
|
+
selected: score.outputPath === params.draftCandidates.bestPath,
|
|
601
|
+
})),
|
|
602
|
+
coarseToFine: {
|
|
603
|
+
...coarseSummaryBase,
|
|
604
|
+
promoted: promotedRows,
|
|
605
|
+
discarded: discardedRows,
|
|
606
|
+
skippedReason: "provider_missing_image_edit_support",
|
|
607
|
+
warnings,
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
if (params.task.target.generationMode === "edit-first") {
|
|
612
|
+
warnings.push("Coarse-to-fine refinement is skipped for edit-first targets.");
|
|
613
|
+
return {
|
|
614
|
+
bestPath: params.draftCandidates.bestPath,
|
|
615
|
+
candidateOutputs: params.draftCandidates.candidateOutputs,
|
|
616
|
+
scores: draftScoresDecorated.map((score) => ({
|
|
617
|
+
...score,
|
|
618
|
+
selected: score.outputPath === params.draftCandidates.bestPath,
|
|
619
|
+
})),
|
|
620
|
+
coarseToFine: {
|
|
621
|
+
...coarseSummaryBase,
|
|
622
|
+
promoted: promotedRows,
|
|
623
|
+
discarded: discardedRows,
|
|
624
|
+
skippedReason: "target_already_edit_first",
|
|
625
|
+
warnings,
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const refinedOutputs = [];
|
|
630
|
+
const sourceByRefinedOutput = new Map();
|
|
631
|
+
for (const [index, promoted] of promotedRows.entries()) {
|
|
632
|
+
const refineTarget = createRefineTarget({
|
|
633
|
+
target: params.task.target,
|
|
634
|
+
sourceOutputPath: promoted.outputPath,
|
|
635
|
+
outDir: params.runContext.outDir,
|
|
636
|
+
finalQuality,
|
|
637
|
+
refineIndex: index + 1,
|
|
638
|
+
});
|
|
639
|
+
const refineJobs = await params.provider.prepareJobs([refineTarget], {
|
|
640
|
+
outDir: params.runContext.outDir,
|
|
641
|
+
imagesDir: params.runContext.imagesDir,
|
|
642
|
+
now: params.runContext.now,
|
|
643
|
+
});
|
|
644
|
+
if (refineJobs.length === 0) {
|
|
645
|
+
throw new ProviderError({
|
|
646
|
+
provider: params.providerName,
|
|
647
|
+
code: "coarse_to_fine_no_refine_job",
|
|
648
|
+
message: `Provider ${params.providerName} did not produce refine job for ${params.task.target.id}.`,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
const refineJob = refineJobs[0];
|
|
652
|
+
const refineRunResult = await params.provider.runJob(refineJob, {
|
|
653
|
+
outDir: params.runContext.outDir,
|
|
654
|
+
imagesDir: params.runContext.imagesDir,
|
|
655
|
+
now: params.runContext.now,
|
|
656
|
+
fetchImpl: params.runContext.fetchImpl,
|
|
657
|
+
});
|
|
658
|
+
const refineSelection = await scoreProviderRunCandidates({
|
|
659
|
+
target: params.task.target,
|
|
660
|
+
runResult: refineRunResult,
|
|
661
|
+
outDir: params.runContext.outDir,
|
|
662
|
+
scoreOptions: COARSE_TO_FINE_DRAFT_SCORING_OPTIONS,
|
|
663
|
+
});
|
|
664
|
+
await copyIfDifferent(refineSelection.bestPath, refineJob.outPath);
|
|
665
|
+
const refinedStat = await stat(refineJob.outPath);
|
|
666
|
+
refinedOutputs.push({
|
|
667
|
+
outputPath: refineJob.outPath,
|
|
668
|
+
bytesWritten: refinedStat.size,
|
|
669
|
+
});
|
|
670
|
+
sourceByRefinedOutput.set(refineJob.outPath, promoted.outputPath);
|
|
671
|
+
promoted.refinedOutputPath = refineJob.outPath;
|
|
672
|
+
}
|
|
673
|
+
const finalRefineSelection = await scoreCandidateImages(params.task.target, refinedOutputs.map((row) => row.outputPath), { outDir: params.runContext.outDir });
|
|
674
|
+
const refineScoresDecorated = finalRefineSelection.scores.map((score) => ({
|
|
675
|
+
...score,
|
|
676
|
+
stage: "refine",
|
|
677
|
+
promoted: true,
|
|
678
|
+
sourceOutputPath: sourceByRefinedOutput.get(score.outputPath),
|
|
679
|
+
}));
|
|
680
|
+
return {
|
|
681
|
+
bestPath: finalRefineSelection.bestPath,
|
|
682
|
+
candidateOutputs: [...params.draftCandidates.candidateOutputs, ...refinedOutputs],
|
|
683
|
+
scores: [...draftScoresDecorated, ...refineScoresDecorated],
|
|
684
|
+
coarseToFine: {
|
|
685
|
+
...coarseSummaryBase,
|
|
686
|
+
promoted: promotedRows,
|
|
687
|
+
discarded: discardedRows,
|
|
688
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
async function runAgenticAutoCorrection(params) {
|
|
693
|
+
const policy = params.normalizedPolicy.agenticRetry;
|
|
694
|
+
if (!policy?.enabled) {
|
|
695
|
+
return params.initialCandidates;
|
|
696
|
+
}
|
|
697
|
+
const summary = {
|
|
698
|
+
enabled: true,
|
|
699
|
+
maxRetries: policy.maxRetries,
|
|
700
|
+
attempted: 0,
|
|
701
|
+
succeeded: false,
|
|
702
|
+
attempts: [],
|
|
703
|
+
};
|
|
704
|
+
if (policy.maxRetries <= 0) {
|
|
705
|
+
summary.skippedReason = "max_retries_zero";
|
|
706
|
+
return {
|
|
707
|
+
...params.initialCandidates,
|
|
708
|
+
agenticRetry: summary,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
let current = ensureSingleSelectedScore(params.initialCandidates);
|
|
712
|
+
const initialSelected = getSelectedCandidate(current.scores);
|
|
713
|
+
const initialTrigger = initialSelected
|
|
714
|
+
? resolveAutoCorrectionTrigger(initialSelected)
|
|
715
|
+
: undefined;
|
|
716
|
+
if (!initialSelected || !initialTrigger) {
|
|
717
|
+
summary.skippedReason = "no_trigger_conditions";
|
|
718
|
+
summary.succeeded = true;
|
|
719
|
+
return {
|
|
720
|
+
...current,
|
|
721
|
+
agenticRetry: summary,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
if (!params.provider.supports("image-edits")) {
|
|
725
|
+
summary.skippedReason = "provider_missing_image_edit_support";
|
|
726
|
+
return {
|
|
727
|
+
...current,
|
|
728
|
+
agenticRetry: summary,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
for (let attempt = 1; attempt <= policy.maxRetries; attempt += 1) {
|
|
732
|
+
const selectedBefore = getSelectedCandidate(current.scores);
|
|
733
|
+
if (!selectedBefore) {
|
|
734
|
+
summary.skippedReason = "selected_candidate_missing";
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
const trigger = resolveAutoCorrectionTrigger(selectedBefore);
|
|
738
|
+
if (!trigger) {
|
|
739
|
+
summary.succeeded = true;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
summary.attempted += 1;
|
|
743
|
+
const retryTarget = createAutoCorrectionTarget({
|
|
744
|
+
target: params.task.target,
|
|
745
|
+
sourceOutputPath: selectedBefore.outputPath,
|
|
746
|
+
outDir: params.runContext.outDir,
|
|
747
|
+
quality: params.normalizedPolicy.finalQuality ?? params.normalizedPolicy.quality,
|
|
748
|
+
attempt,
|
|
749
|
+
critique: trigger.critique,
|
|
750
|
+
});
|
|
751
|
+
const retryJobs = await params.provider.prepareJobs([retryTarget], {
|
|
752
|
+
outDir: params.runContext.outDir,
|
|
753
|
+
imagesDir: params.runContext.imagesDir,
|
|
754
|
+
now: params.runContext.now,
|
|
755
|
+
});
|
|
756
|
+
if (retryJobs.length === 0) {
|
|
757
|
+
summary.skippedReason = "provider_no_retry_job";
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
const retryJob = retryJobs[0];
|
|
761
|
+
const retryRunResult = await params.provider.runJob(retryJob, {
|
|
762
|
+
outDir: params.runContext.outDir,
|
|
763
|
+
imagesDir: params.runContext.imagesDir,
|
|
764
|
+
now: params.runContext.now,
|
|
765
|
+
fetchImpl: params.runContext.fetchImpl,
|
|
766
|
+
});
|
|
767
|
+
const scoredRetry = await scoreProviderRunCandidates({
|
|
768
|
+
target: params.task.target,
|
|
769
|
+
runResult: retryRunResult,
|
|
770
|
+
outDir: params.runContext.outDir,
|
|
771
|
+
});
|
|
772
|
+
const retryScores = scoredRetry.scores.map((score) => ({
|
|
773
|
+
...score,
|
|
774
|
+
stage: "autocorrect",
|
|
775
|
+
autoCorrectAttempt: attempt,
|
|
776
|
+
sourceOutputPath: selectedBefore.outputPath,
|
|
777
|
+
selected: false,
|
|
778
|
+
}));
|
|
779
|
+
current = mergeScoredCandidateSets(current, {
|
|
780
|
+
candidateOutputs: scoredRetry.candidateOutputs,
|
|
781
|
+
scores: retryScores,
|
|
782
|
+
bestPath: scoredRetry.bestPath,
|
|
783
|
+
});
|
|
784
|
+
const selectedAfter = getSelectedCandidate(current.scores);
|
|
785
|
+
if (!selectedAfter) {
|
|
786
|
+
summary.skippedReason = "retry_selected_candidate_missing";
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
const retryOutputPath = retryScores[0]?.outputPath ?? selectedAfter.outputPath;
|
|
790
|
+
summary.attempts.push({
|
|
791
|
+
attempt,
|
|
792
|
+
sourceOutputPath: selectedBefore.outputPath,
|
|
793
|
+
outputPath: selectedAfter.outputPath,
|
|
794
|
+
retryOutputPath,
|
|
795
|
+
critique: trigger.critique,
|
|
796
|
+
triggeredBy: trigger.triggeredBy,
|
|
797
|
+
scoreBefore: selectedBefore.score,
|
|
798
|
+
scoreAfter: selectedAfter.score,
|
|
799
|
+
passedBefore: selectedBefore.passedAcceptance,
|
|
800
|
+
passedAfter: selectedAfter.passedAcceptance,
|
|
801
|
+
reasonsBefore: selectedBefore.reasons,
|
|
802
|
+
reasonsAfter: selectedAfter.reasons,
|
|
803
|
+
});
|
|
804
|
+
if (!resolveAutoCorrectionTrigger(selectedAfter)) {
|
|
805
|
+
summary.succeeded = true;
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
...current,
|
|
811
|
+
agenticRetry: summary,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function mergeScoredCandidateSets(base, delta) {
|
|
815
|
+
const outputsByPath = new Map(base.candidateOutputs.map((row) => [row.outputPath, row]));
|
|
816
|
+
for (const row of delta.candidateOutputs) {
|
|
817
|
+
outputsByPath.set(row.outputPath, row);
|
|
818
|
+
}
|
|
819
|
+
const scores = [...base.scores, ...delta.scores];
|
|
820
|
+
scores.sort((left, right) => {
|
|
821
|
+
if (left.passedAcceptance !== right.passedAcceptance) {
|
|
822
|
+
return left.passedAcceptance ? -1 : 1;
|
|
823
|
+
}
|
|
824
|
+
if (left.score !== right.score) {
|
|
825
|
+
return right.score - left.score;
|
|
826
|
+
}
|
|
827
|
+
return left.outputPath.localeCompare(right.outputPath);
|
|
828
|
+
});
|
|
829
|
+
return ensureSingleSelectedScore({
|
|
830
|
+
candidateOutputs: Array.from(outputsByPath.values()).sort((left, right) => left.outputPath.localeCompare(right.outputPath)),
|
|
831
|
+
scores,
|
|
832
|
+
bestPath: scores[0]?.outputPath ?? base.bestPath,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
function ensureSingleSelectedScore(set) {
|
|
836
|
+
if (set.scores.length === 0) {
|
|
837
|
+
return set;
|
|
838
|
+
}
|
|
839
|
+
const selectedPath = set.scores[0].outputPath;
|
|
840
|
+
const scores = set.scores.map((score) => ({
|
|
841
|
+
...score,
|
|
842
|
+
selected: Boolean(selectedPath && score.outputPath === selectedPath),
|
|
843
|
+
}));
|
|
844
|
+
return {
|
|
845
|
+
...set,
|
|
846
|
+
scores,
|
|
847
|
+
bestPath: selectedPath,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function getSelectedCandidate(scores) {
|
|
851
|
+
return scores.find((score) => score.selected) ?? scores[0];
|
|
852
|
+
}
|
|
853
|
+
function resolveAutoCorrectionTrigger(score) {
|
|
854
|
+
const triggeredBy = dedupeTargetIdList(score.reasons.filter((reason) => AGENTIC_RETRY_TRIGGER_REASONS.has(reason)));
|
|
855
|
+
if (triggeredBy.length === 0) {
|
|
856
|
+
return undefined;
|
|
857
|
+
}
|
|
858
|
+
const critiqueParts = [
|
|
859
|
+
"Refine this image to pass hard quality gates while preserving composition, silhouette, and style.",
|
|
860
|
+
];
|
|
861
|
+
if (triggeredBy.includes("vlm_gate_below_threshold")) {
|
|
862
|
+
critiqueParts.push(`VLM critique: ${score.vlm?.reason ?? "Improve framing and subject clarity against rubric expectations."}`);
|
|
863
|
+
}
|
|
864
|
+
if (triggeredBy.includes("alpha_halo_risk_exceeded")) {
|
|
865
|
+
critiqueParts.push("Remove edge haloing and color fringe around transparent boundaries.");
|
|
866
|
+
}
|
|
867
|
+
if (triggeredBy.includes("alpha_stray_noise_exceeded")) {
|
|
868
|
+
critiqueParts.push("Eliminate isolated stray alpha noise and detached transparency speckles.");
|
|
869
|
+
}
|
|
870
|
+
if (triggeredBy.includes("alpha_edge_sharpness_too_low")) {
|
|
871
|
+
critiqueParts.push("Increase alpha-edge sharpness while keeping clean anti-aliased contours.");
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
critique: critiqueParts.join(" "),
|
|
875
|
+
triggeredBy,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function createAutoCorrectionTarget(params) {
|
|
879
|
+
const relativeSource = toPortableRelativePath(params.outDir, params.sourceOutputPath);
|
|
880
|
+
return {
|
|
881
|
+
...params.target,
|
|
882
|
+
out: withAutoCorrectSuffix(params.target.out, params.attempt),
|
|
883
|
+
generationMode: "edit-first",
|
|
884
|
+
generationPolicy: {
|
|
885
|
+
...(params.target.generationPolicy ?? {}),
|
|
886
|
+
quality: params.quality,
|
|
887
|
+
candidates: 1,
|
|
888
|
+
},
|
|
889
|
+
edit: {
|
|
890
|
+
mode: "iterate",
|
|
891
|
+
instruction: params.critique,
|
|
892
|
+
preserveComposition: true,
|
|
893
|
+
inputs: [
|
|
894
|
+
{
|
|
895
|
+
path: relativeSource,
|
|
896
|
+
role: "base",
|
|
897
|
+
fidelity: "high",
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
},
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function createRefineTarget(params) {
|
|
904
|
+
const relativeSource = toPortableRelativePath(params.outDir, params.sourceOutputPath);
|
|
905
|
+
return {
|
|
906
|
+
...params.target,
|
|
907
|
+
out: withRefineSuffix(params.target.out, params.refineIndex),
|
|
908
|
+
generationMode: "edit-first",
|
|
909
|
+
generationPolicy: {
|
|
910
|
+
...(params.target.generationPolicy ?? {}),
|
|
911
|
+
quality: params.finalQuality,
|
|
912
|
+
candidates: 1,
|
|
913
|
+
},
|
|
914
|
+
edit: {
|
|
915
|
+
mode: "iterate",
|
|
916
|
+
instruction: params.target.edit?.instruction ??
|
|
917
|
+
"Refine the supplied draft candidate at higher fidelity while preserving composition and silhouette.",
|
|
918
|
+
preserveComposition: params.target.edit?.preserveComposition ?? true,
|
|
919
|
+
inputs: [
|
|
920
|
+
{
|
|
921
|
+
path: relativeSource,
|
|
922
|
+
role: "base",
|
|
923
|
+
fidelity: "high",
|
|
924
|
+
},
|
|
925
|
+
],
|
|
926
|
+
},
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function withRefineSuffix(filePath, refineIndex) {
|
|
930
|
+
const ext = path.extname(filePath);
|
|
931
|
+
const base = filePath.slice(0, filePath.length - ext.length);
|
|
932
|
+
return `${base}.refine-${refineIndex}${ext}`;
|
|
933
|
+
}
|
|
934
|
+
function withAutoCorrectSuffix(filePath, attempt) {
|
|
935
|
+
const ext = path.extname(filePath);
|
|
936
|
+
const base = filePath.slice(0, filePath.length - ext.length);
|
|
937
|
+
return `${base}.autocorrect-${attempt}${ext}`;
|
|
938
|
+
}
|
|
939
|
+
function toPortableRelativePath(rootDir, filePath) {
|
|
940
|
+
const relative = path.relative(rootDir, filePath);
|
|
941
|
+
return relative.split(path.sep).join("/");
|
|
942
|
+
}
|
|
943
|
+
async function copyIfDifferent(sourcePath, destinationPath) {
|
|
944
|
+
if (sourcePath === destinationPath) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
await cp(sourcePath, destinationPath, { force: true });
|
|
948
|
+
}
|
|
949
|
+
function backoffMsForAttempt(attempt) {
|
|
950
|
+
return Math.min(5000, 300 * 2 ** (attempt - 1));
|
|
951
|
+
}
|
|
952
|
+
function targetRequiresEditSupport(target) {
|
|
953
|
+
if (target.generationMode !== "edit-first") {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
return (target.edit?.inputs?.length ?? 0) > 0;
|
|
957
|
+
}
|
|
958
|
+
function computeProviderDelayMs(task, fallbackDelay) {
|
|
959
|
+
const requestedRateLimit = task.target.generationPolicy?.rateLimitPerMinute;
|
|
960
|
+
if (typeof requestedRateLimit === "number" &&
|
|
961
|
+
Number.isFinite(requestedRateLimit) &&
|
|
962
|
+
requestedRateLimit > 0) {
|
|
963
|
+
const rateDelay = Math.ceil(60000 / requestedRateLimit);
|
|
964
|
+
return Math.max(rateDelay, fallbackDelay);
|
|
965
|
+
}
|
|
966
|
+
return fallbackDelay;
|
|
967
|
+
}
|
|
968
|
+
async function resolveStyleReferenceLineage(params) {
|
|
969
|
+
const lineage = [];
|
|
970
|
+
for (const styleReferenceImage of params.task.target.styleReferenceImages ?? []) {
|
|
971
|
+
lineage.push({
|
|
972
|
+
source: "style-kit",
|
|
973
|
+
reference: styleReferenceImage,
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
for (const sourceTargetId of params.task.target.styleReferenceFrom ?? []) {
|
|
977
|
+
const sourceTask = params.taskById.get(sourceTargetId);
|
|
978
|
+
const sourceTarget = sourceTask?.target ?? params.allTargetsById.get(sourceTargetId);
|
|
979
|
+
if (!sourceTarget) {
|
|
980
|
+
throw new Error(`Target "${params.task.target.id}" chains style references from "${sourceTargetId}", but no matching planned target exists.`);
|
|
981
|
+
}
|
|
982
|
+
const sourceOutputPath = path.resolve(params.imagesDir, sourceTarget.out.split("/").join(path.sep));
|
|
983
|
+
if (!sourceTask && !(await fileExists(sourceOutputPath))) {
|
|
984
|
+
throw new Error(`Target "${params.task.target.id}" chains style references from "${sourceTargetId}", but ${sourceOutputPath} does not exist. Generate that target first or include it in the current run.`);
|
|
985
|
+
}
|
|
986
|
+
lineage.push({
|
|
987
|
+
source: "target-output",
|
|
988
|
+
reference: sourceTarget.out,
|
|
989
|
+
sourceTargetId,
|
|
990
|
+
resolvedPath: sourceOutputPath,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
return lineage.length > 0 ? lineage : undefined;
|
|
994
|
+
}
|
|
995
|
+
async function sliceStripIntoFrames(stripPath, strip, imagesDir) {
|
|
996
|
+
// Read the full strip into a buffer first because frameOutputPaths[0]
|
|
997
|
+
// points to the same file as stripPath (frame 0 IS the strip target).
|
|
998
|
+
// Sharp cannot read and write the same file simultaneously.
|
|
999
|
+
const stripBuffer = await readFile(stripPath);
|
|
1000
|
+
const meta = await sharp(stripBuffer).metadata();
|
|
1001
|
+
if (!meta.width || !meta.height)
|
|
1002
|
+
return;
|
|
1003
|
+
const frameWidth = Math.floor(meta.width / strip.frameCount);
|
|
1004
|
+
for (let i = 0; i < strip.frameCount; i += 1) {
|
|
1005
|
+
const framePath = path.resolve(imagesDir, strip.frameOutputPaths[i]);
|
|
1006
|
+
await mkdir(path.dirname(framePath), { recursive: true });
|
|
1007
|
+
await sharp(stripBuffer)
|
|
1008
|
+
.extract({
|
|
1009
|
+
left: i * frameWidth,
|
|
1010
|
+
top: 0,
|
|
1011
|
+
width: i < strip.frameCount - 1 ? frameWidth : meta.width - i * frameWidth,
|
|
1012
|
+
height: meta.height,
|
|
1013
|
+
})
|
|
1014
|
+
.png()
|
|
1015
|
+
.toFile(framePath);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function dedupeTargetIdList(targetIds) {
|
|
1019
|
+
const deduped = [];
|
|
1020
|
+
const seen = new Set();
|
|
1021
|
+
for (const targetId of targetIds) {
|
|
1022
|
+
if (seen.has(targetId)) {
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
seen.add(targetId);
|
|
1026
|
+
deduped.push(targetId);
|
|
1027
|
+
}
|
|
1028
|
+
return deduped;
|
|
1029
|
+
}
|
|
1030
|
+
function resolveManifestPathFromIndex(index, targetsIndexPath) {
|
|
1031
|
+
if (typeof index.manifestPath !== "string") {
|
|
1032
|
+
return undefined;
|
|
1033
|
+
}
|
|
1034
|
+
const trimmed = index.manifestPath.trim();
|
|
1035
|
+
if (!trimmed) {
|
|
1036
|
+
return undefined;
|
|
1037
|
+
}
|
|
1038
|
+
if (path.isAbsolute(trimmed)) {
|
|
1039
|
+
return trimmed;
|
|
1040
|
+
}
|
|
1041
|
+
return path.resolve(path.dirname(targetsIndexPath), trimmed);
|
|
1042
|
+
}
|
|
1043
|
+
function delay(ms) {
|
|
1044
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1045
|
+
}
|
|
1046
|
+
async function readSelectionLock(filePath) {
|
|
1047
|
+
try {
|
|
1048
|
+
const raw = await readFile(filePath, "utf8");
|
|
1049
|
+
const parsed = JSON.parse(raw);
|
|
1050
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1051
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1052
|
+
throw new Error(`selection lock file content is not a JSON object (${filePath}).`);
|
|
1053
|
+
}
|
|
1054
|
+
return parsed;
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
if (isNoSuchFileError(error)) {
|
|
1058
|
+
return {};
|
|
1059
|
+
}
|
|
1060
|
+
throw new Error(`Failed to parse selection lock JSON (${filePath}): ${error instanceof Error ? error.message : String(error)}`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function isNoSuchFileError(error) {
|
|
1064
|
+
return (Boolean(error) && typeof error === "object" && error.code === "ENOENT");
|
|
1065
|
+
}
|
|
1066
|
+
async function fileExists(filePath) {
|
|
1067
|
+
try {
|
|
1068
|
+
await access(filePath);
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
catch {
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
async function collectExistingTargetOutputIds(targets, imagesDir) {
|
|
1076
|
+
const existingIds = new Set();
|
|
1077
|
+
const checks = await Promise.all(targets.map(async (target) => ({
|
|
1078
|
+
targetId: target.id,
|
|
1079
|
+
exists: await fileExists(path.resolve(imagesDir, target.out.split("/").join(path.sep))),
|
|
1080
|
+
})));
|
|
1081
|
+
for (const check of checks) {
|
|
1082
|
+
if (check.exists) {
|
|
1083
|
+
existingIds.add(check.targetId);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return existingIds;
|
|
1087
|
+
}
|
|
1088
|
+
//# sourceMappingURL=generate.js.map
|