@geotechcli/core 0.4.77 → 0.4.79
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/dist/agents/data-tools.js +38 -1
- package/dist/agents/data-tools.js.map +1 -1
- package/dist/agents/fem-artifact-guards.d.ts +14 -0
- package/dist/agents/fem-artifact-guards.d.ts.map +1 -0
- package/dist/agents/fem-artifact-guards.js +53 -0
- package/dist/agents/fem-artifact-guards.js.map +1 -0
- package/dist/agents/fem-tools.js +86 -1
- package/dist/agents/fem-tools.js.map +1 -1
- package/dist/agents/filesystem-tools.js +13 -0
- package/dist/agents/filesystem-tools.js.map +1 -1
- package/dist/agents/provider-operating-contract.d.ts +3 -3
- package/dist/agents/provider-operating-contract.d.ts.map +1 -1
- package/dist/agents/provider-operating-contract.js +10 -46
- package/dist/agents/provider-operating-contract.js.map +1 -1
- package/dist/agents/runtime-bootstrap.d.ts +1 -0
- package/dist/agents/runtime-bootstrap.d.ts.map +1 -1
- package/dist/agents/runtime-bootstrap.js +1 -0
- package/dist/agents/runtime-bootstrap.js.map +1 -1
- package/dist/agents/safety.d.ts.map +1 -1
- package/dist/agents/safety.js +33 -0
- package/dist/agents/safety.js.map +1 -1
- package/dist/agents/signal-tools.d.ts +2 -0
- package/dist/agents/signal-tools.d.ts.map +1 -0
- package/dist/agents/signal-tools.js +96 -0
- package/dist/agents/signal-tools.js.map +1 -0
- package/dist/agents/swarm-planner.js +1 -1
- package/dist/agents/swarm-planner.js.map +1 -1
- package/dist/agents/swarm.d.ts +1 -0
- package/dist/agents/swarm.d.ts.map +1 -1
- package/dist/agents/swarm.js +202 -31
- package/dist/agents/swarm.js.map +1 -1
- package/dist/fem/ground-model-draft.d.ts +14 -0
- package/dist/fem/ground-model-draft.d.ts.map +1 -1
- package/dist/fem/ground-model-draft.js +86 -21
- package/dist/fem/ground-model-draft.js.map +1 -1
- package/dist/fem/index.d.ts +1 -1
- package/dist/fem/index.d.ts.map +1 -1
- package/dist/fem/index.js +1 -1
- package/dist/fem/index.js.map +1 -1
- package/dist/fem/routing.d.ts +15 -1
- package/dist/fem/routing.d.ts.map +1 -1
- package/dist/fem/routing.js +192 -14
- package/dist/fem/routing.js.map +1 -1
- package/dist/fem/validation.d.ts.map +1 -1
- package/dist/fem/validation.js +715 -30
- package/dist/fem/validation.js.map +1 -1
- package/dist/fem/webgl.d.ts.map +1 -1
- package/dist/fem/webgl.js +24 -8
- package/dist/fem/webgl.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ingest/document-evidence-packet.d.ts +603 -45
- package/dist/ingest/document-evidence-packet.d.ts.map +1 -1
- package/dist/ingest/document-evidence-packet.js +145 -5
- package/dist/ingest/document-evidence-packet.js.map +1 -1
- package/dist/ingest/geotech-benchmark-corpus.d.ts +108 -0
- package/dist/ingest/geotech-benchmark-corpus.d.ts.map +1 -0
- package/dist/ingest/geotech-benchmark-corpus.js +437 -0
- package/dist/ingest/geotech-benchmark-corpus.js.map +1 -0
- package/dist/ingest/geotech-document-benchmark.d.ts +133 -0
- package/dist/ingest/geotech-document-benchmark.d.ts.map +1 -1
- package/dist/ingest/geotech-document-benchmark.js +370 -2
- package/dist/ingest/geotech-document-benchmark.js.map +1 -1
- package/dist/ingest/geotech-document.d.ts +3 -0
- package/dist/ingest/geotech-document.d.ts.map +1 -1
- package/dist/ingest/geotech-document.js +7 -0
- package/dist/ingest/geotech-document.js.map +1 -1
- package/dist/ingest/index.d.ts +2 -1
- package/dist/ingest/index.d.ts.map +1 -1
- package/dist/ingest/index.js +1 -0
- package/dist/ingest/index.js.map +1 -1
- package/dist/ingest/job-store.d.ts.map +1 -1
- package/dist/ingest/job-store.js +196 -0
- package/dist/ingest/job-store.js.map +1 -1
- package/dist/ingest/job-worker.d.ts.map +1 -1
- package/dist/ingest/job-worker.js +5 -0
- package/dist/ingest/job-worker.js.map +1 -1
- package/dist/ingest/page-evidence-cache.d.ts +6 -2
- package/dist/ingest/page-evidence-cache.d.ts.map +1 -1
- package/dist/ingest/page-evidence-cache.js +229 -4
- package/dist/ingest/page-evidence-cache.js.map +1 -1
- package/dist/ingest/pdf.d.ts.map +1 -1
- package/dist/ingest/pdf.js +2 -2
- package/dist/ingest/pdf.js.map +1 -1
- package/dist/ingest/review-store.d.ts +3 -0
- package/dist/ingest/review-store.d.ts.map +1 -1
- package/dist/ingest/review-store.js +28 -0
- package/dist/ingest/review-store.js.map +1 -1
- package/dist/llm/capabilities.d.ts +6 -1
- package/dist/llm/capabilities.d.ts.map +1 -1
- package/dist/llm/capabilities.js +66 -0
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/index.d.ts +2 -2
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +1 -1
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/types.d.ts +20 -0
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/llm/types.js.map +1 -1
- package/dist/meta/metadata.json +1 -1
- package/dist/report/ingest-dossier.d.ts.map +1 -1
- package/dist/report/ingest-dossier.js +13 -1
- package/dist/report/ingest-dossier.js.map +1 -1
- package/dist/signal/index.d.ts +95 -0
- package/dist/signal/index.d.ts.map +1 -0
- package/dist/signal/index.js +375 -0
- package/dist/signal/index.js.map +1 -0
- package/dist/verifier/findings.d.ts +1 -1
- package/dist/verifier/findings.d.ts.map +1 -1
- package/dist/verifier/findings.js +329 -0
- package/dist/verifier/findings.js.map +1 -1
- package/dist/vision/ocr.d.ts +2 -0
- package/dist/vision/ocr.d.ts.map +1 -1
- package/dist/vision/ocr.js +148 -2
- package/dist/vision/ocr.js.map +1 -1
- package/dist/vision/preprocess.d.ts +66 -1
- package/dist/vision/preprocess.d.ts.map +1 -1
- package/dist/vision/preprocess.js +728 -9
- package/dist/vision/preprocess.js.map +1 -1
- package/dist/workspace/project-workflow-executor.d.ts +1 -1
- package/dist/workspace/project-workflow-executor.d.ts.map +1 -1
- package/dist/workspace/project-workflow-executor.js +216 -4
- package/dist/workspace/project-workflow-executor.js.map +1 -1
- package/dist/workspace/project-workflow-router.d.ts.map +1 -1
- package/dist/workspace/project-workflow-router.js +41 -1
- package/dist/workspace/project-workflow-router.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { createRequire } from 'node:module';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
+
export const VISION_IMAGE_PREPROCESS_METADATA_SCHEMA_VERSION = 1;
|
|
6
|
+
export const VISION_IMAGE_PREPROCESS_PIPELINE_VERSION = 'vision-image-preprocess-v2';
|
|
4
7
|
const require = createRequire(import.meta.url);
|
|
5
8
|
let cachedPdfRendererModule = null;
|
|
6
9
|
let cachedCanvasFactory = null;
|
|
@@ -16,6 +19,625 @@ async function loadSharp() {
|
|
|
16
19
|
return null;
|
|
17
20
|
}
|
|
18
21
|
}
|
|
22
|
+
async function readImageMetadata(sharp, buffer, mimeType) {
|
|
23
|
+
const base = {
|
|
24
|
+
mimeType,
|
|
25
|
+
byteLength: Buffer.byteLength(Buffer.from(buffer)),
|
|
26
|
+
};
|
|
27
|
+
if (!sharp) {
|
|
28
|
+
return base;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const metadata = await sharp(Buffer.from(buffer), { pages: 1 }).metadata();
|
|
32
|
+
return {
|
|
33
|
+
...base,
|
|
34
|
+
...(Number.isFinite(metadata.width) ? { width: metadata.width } : {}),
|
|
35
|
+
...(Number.isFinite(metadata.height) ? { height: metadata.height } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return base;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function buildPreprocessingMetadata(input) {
|
|
43
|
+
const regions = input.regions?.length
|
|
44
|
+
? input.regions
|
|
45
|
+
: [{
|
|
46
|
+
id: input.transformed ? 'normalized-full-page' : 'original-full-page',
|
|
47
|
+
source: 'preprocessing',
|
|
48
|
+
label: input.transformed ? 'normalized full page' : 'original full page',
|
|
49
|
+
bbox2d: [0, 0, 1, 1],
|
|
50
|
+
coverageRatio: 1,
|
|
51
|
+
}];
|
|
52
|
+
return {
|
|
53
|
+
schemaVersion: VISION_IMAGE_PREPROCESS_METADATA_SCHEMA_VERSION,
|
|
54
|
+
pipelineVersion: VISION_IMAGE_PREPROCESS_PIPELINE_VERSION,
|
|
55
|
+
policy: input.policy,
|
|
56
|
+
transformed: input.transformed,
|
|
57
|
+
input: input.input,
|
|
58
|
+
output: input.output,
|
|
59
|
+
operations: [...new Set(input.operations.map((operation) => operation.trim()).filter(Boolean))],
|
|
60
|
+
regions,
|
|
61
|
+
...(input.quality ? { quality: input.quality } : {}),
|
|
62
|
+
warnings: [...new Set(input.warnings.map((warning) => warning.trim()).filter(Boolean))],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function resolveVisionImagePreprocessPolicy(value = process.env.GEOTECHCLI_PREPROCESSING_MODE) {
|
|
66
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
67
|
+
if (normalized === 'none') {
|
|
68
|
+
return 'none';
|
|
69
|
+
}
|
|
70
|
+
if (normalized === 'region-v2' || normalized === 'region_v2' || normalized === 'regions-v2') {
|
|
71
|
+
return 'region-v2';
|
|
72
|
+
}
|
|
73
|
+
return 'ocr-optimized';
|
|
74
|
+
}
|
|
75
|
+
const DEFAULT_DESKEW = {
|
|
76
|
+
method: 'projection-profile',
|
|
77
|
+
angleDeg: 0,
|
|
78
|
+
confidence: 0,
|
|
79
|
+
applied: false,
|
|
80
|
+
};
|
|
81
|
+
async function readRawPreprocessImage(sharp, buffer) {
|
|
82
|
+
const { data, info } = await sharp(buffer, { pages: 1 })
|
|
83
|
+
.grayscale()
|
|
84
|
+
.raw()
|
|
85
|
+
.toBuffer({ resolveWithObject: true });
|
|
86
|
+
const width = Number(info.width);
|
|
87
|
+
const height = Number(info.height);
|
|
88
|
+
const channels = Number(info.channels) || 1;
|
|
89
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 32 || height < 32) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const darkThreshold = 238;
|
|
93
|
+
const rowDarkCounts = new Array(height).fill(0);
|
|
94
|
+
const colDarkCounts = new Array(width).fill(0);
|
|
95
|
+
let minX = width;
|
|
96
|
+
let minY = height;
|
|
97
|
+
let maxX = -1;
|
|
98
|
+
let maxY = -1;
|
|
99
|
+
let darkCount = 0;
|
|
100
|
+
for (let y = 0; y < height; y += 1) {
|
|
101
|
+
for (let x = 0; x < width; x += 1) {
|
|
102
|
+
const offset = (y * width + x) * channels;
|
|
103
|
+
const value = data[offset] ?? 255;
|
|
104
|
+
if (value < darkThreshold) {
|
|
105
|
+
rowDarkCounts[y] += 1;
|
|
106
|
+
colDarkCounts[x] += 1;
|
|
107
|
+
darkCount += 1;
|
|
108
|
+
if (x < minX)
|
|
109
|
+
minX = x;
|
|
110
|
+
if (x > maxX)
|
|
111
|
+
maxX = x;
|
|
112
|
+
if (y < minY)
|
|
113
|
+
minY = y;
|
|
114
|
+
if (y > maxY)
|
|
115
|
+
maxY = y;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
data,
|
|
121
|
+
width,
|
|
122
|
+
height,
|
|
123
|
+
channels,
|
|
124
|
+
rowDarkCounts,
|
|
125
|
+
colDarkCounts,
|
|
126
|
+
darkCount,
|
|
127
|
+
minX,
|
|
128
|
+
minY,
|
|
129
|
+
maxX,
|
|
130
|
+
maxY,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function estimateDeskewCorrection(sharp, buffer, options = {}) {
|
|
134
|
+
try {
|
|
135
|
+
const raw = await readRawPreprocessImage(sharp, buffer);
|
|
136
|
+
if (!raw || raw.darkCount < 80 || raw.maxX <= raw.minX || raw.maxY <= raw.minY) {
|
|
137
|
+
return DEFAULT_DESKEW;
|
|
138
|
+
}
|
|
139
|
+
const points = [];
|
|
140
|
+
const targetSamples = 75000;
|
|
141
|
+
const stride = Math.max(1, Math.ceil(Math.sqrt((raw.width * raw.height) / targetSamples)));
|
|
142
|
+
for (let y = raw.minY; y <= raw.maxY; y += stride) {
|
|
143
|
+
for (let x = raw.minX; x <= raw.maxX; x += stride) {
|
|
144
|
+
const value = raw.data[(y * raw.width + x) * raw.channels] ?? 255;
|
|
145
|
+
if (value < 210) {
|
|
146
|
+
points.push([x - raw.width / 2, y - raw.height / 2]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (points.length < 80) {
|
|
151
|
+
return DEFAULT_DESKEW;
|
|
152
|
+
}
|
|
153
|
+
let bestAngle = 0;
|
|
154
|
+
let bestScore = -Infinity;
|
|
155
|
+
let secondBestScore = -Infinity;
|
|
156
|
+
const scoreAngle = (angle) => {
|
|
157
|
+
const radians = (angle * Math.PI) / 180;
|
|
158
|
+
const sin = Math.sin(radians);
|
|
159
|
+
const cos = Math.cos(radians);
|
|
160
|
+
const rowBins = new Map();
|
|
161
|
+
const colBins = new Map();
|
|
162
|
+
for (const [x, y] of points) {
|
|
163
|
+
const rotatedX = x * cos - y * sin;
|
|
164
|
+
const rotatedY = x * sin + y * cos;
|
|
165
|
+
const rowBin = Math.round(rotatedY / 2);
|
|
166
|
+
const colBin = Math.round(rotatedX / 2);
|
|
167
|
+
rowBins.set(rowBin, (rowBins.get(rowBin) ?? 0) + 1);
|
|
168
|
+
colBins.set(colBin, (colBins.get(colBin) ?? 0) + 1);
|
|
169
|
+
}
|
|
170
|
+
let score = 0;
|
|
171
|
+
for (const count of rowBins.values()) {
|
|
172
|
+
score += count * count;
|
|
173
|
+
}
|
|
174
|
+
for (const count of colBins.values()) {
|
|
175
|
+
score += count * count;
|
|
176
|
+
}
|
|
177
|
+
return score;
|
|
178
|
+
};
|
|
179
|
+
for (let angle = -4; angle <= 4.0001; angle += 0.25) {
|
|
180
|
+
const score = scoreAngle(angle);
|
|
181
|
+
if (score > bestScore) {
|
|
182
|
+
secondBestScore = bestScore;
|
|
183
|
+
bestScore = score;
|
|
184
|
+
bestAngle = angle;
|
|
185
|
+
}
|
|
186
|
+
else if (score > secondBestScore) {
|
|
187
|
+
secondBestScore = score;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (options.finePass) {
|
|
191
|
+
const coarseBestAngle = bestAngle;
|
|
192
|
+
for (let angle = coarseBestAngle - 0.3; angle <= coarseBestAngle + 0.3001; angle += 0.05) {
|
|
193
|
+
const rounded = Math.round(angle * 100) / 100;
|
|
194
|
+
const score = scoreAngle(rounded);
|
|
195
|
+
if (score > bestScore) {
|
|
196
|
+
secondBestScore = bestScore;
|
|
197
|
+
bestScore = score;
|
|
198
|
+
bestAngle = rounded;
|
|
199
|
+
}
|
|
200
|
+
else if (score > secondBestScore) {
|
|
201
|
+
secondBestScore = score;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const confidence = bestScore > 0
|
|
206
|
+
? clampRatio((bestScore - Math.max(0, secondBestScore)) / bestScore * 8)
|
|
207
|
+
: 0;
|
|
208
|
+
const applied = Math.abs(bestAngle) >= 0.35 && (confidence >= 0.01 || Math.abs(bestAngle) >= 1);
|
|
209
|
+
return {
|
|
210
|
+
method: options.finePass ? 'projection-profile-fine' : 'projection-profile',
|
|
211
|
+
angleDeg: roundAngle(applied ? bestAngle : 0),
|
|
212
|
+
confidence: roundRatio(confidence),
|
|
213
|
+
applied,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return DEFAULT_DESKEW;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function detectPreprocessingRegions(sharp, buffer, policy = 'ocr-optimized') {
|
|
221
|
+
const regions = [{
|
|
222
|
+
id: 'normalized-full-page',
|
|
223
|
+
source: 'preprocessing',
|
|
224
|
+
label: 'normalized full page',
|
|
225
|
+
bbox2d: [0, 0, 1, 1],
|
|
226
|
+
coverageRatio: 1,
|
|
227
|
+
}];
|
|
228
|
+
try {
|
|
229
|
+
const raw = await readRawPreprocessImage(sharp, buffer);
|
|
230
|
+
if (!raw || raw.darkCount === 0 || raw.maxX <= raw.minX || raw.maxY <= raw.minY) {
|
|
231
|
+
return scorePreprocessRegions(raw, regions);
|
|
232
|
+
}
|
|
233
|
+
const contentRegion = buildRegionFromPixels('content-bounding-box', 'detected content bounding box', raw.minX, raw.minY, raw.maxX, raw.maxY, raw.width, raw.height);
|
|
234
|
+
if (contentRegion.coverageRatio != null && contentRegion.coverageRatio < 0.96) {
|
|
235
|
+
regions.push(contentRegion);
|
|
236
|
+
}
|
|
237
|
+
const panelRegions = buildTableLogPanelCandidates(raw, { regionV2: policy === 'region-v2' });
|
|
238
|
+
for (const region of panelRegions) {
|
|
239
|
+
if (!regions.some((existing) => existing.id !== 'normalized-full-page'
|
|
240
|
+
&& existing.id !== 'original-full-page'
|
|
241
|
+
&& regionOverlap(existing, region) > 0.88)) {
|
|
242
|
+
regions.push(region);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return scorePreprocessRegions(raw, regions);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return regions;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function buildTableLogPanelCandidates(raw, options = {}) {
|
|
252
|
+
const rowThreshold = Math.max(24, Math.round(raw.width * 0.07));
|
|
253
|
+
const colThreshold = Math.max(24, Math.round(raw.height * 0.055));
|
|
254
|
+
const heavyRows = raw.rowDarkCounts
|
|
255
|
+
.map((count, index) => ({ count, index }))
|
|
256
|
+
.filter((row) => row.count >= rowThreshold)
|
|
257
|
+
.map((row) => row.index);
|
|
258
|
+
const heavyCols = raw.colDarkCounts
|
|
259
|
+
.map((count, index) => ({ count, index }))
|
|
260
|
+
.filter((col) => col.count >= colThreshold)
|
|
261
|
+
.map((col) => col.index);
|
|
262
|
+
if (heavyRows.length < 4 || heavyCols.length < 3) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
const rowSpan = mergeLineClusters(groupContiguous(heavyRows, Math.max(3, Math.round(raw.height * 0.004))), Math.round(raw.height * 0.18))
|
|
266
|
+
.reduce((span, group) => {
|
|
267
|
+
if (!span)
|
|
268
|
+
return { ...group };
|
|
269
|
+
return {
|
|
270
|
+
start: Math.min(span.start, group.start),
|
|
271
|
+
end: Math.max(span.end, group.end),
|
|
272
|
+
count: span.count + group.count,
|
|
273
|
+
};
|
|
274
|
+
}, null);
|
|
275
|
+
const colClusters = mergeLineClusters(groupContiguous(heavyCols, Math.max(3, Math.round(raw.width * 0.004))), Math.round(raw.width * 0.14))
|
|
276
|
+
.filter((cluster) => cluster.count >= 3 || cluster.end - cluster.start >= raw.width * 0.08);
|
|
277
|
+
if (!rowSpan || colClusters.length === 0) {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
const full = buildRegionFromPixels('table-log-panel-candidate', 'detected table/log panel candidate', Math.min(...heavyCols), Math.min(...heavyRows), Math.max(...heavyCols), Math.max(...heavyRows), raw.width, raw.height, 8);
|
|
281
|
+
const candidates = [full];
|
|
282
|
+
for (const [index, cluster] of colClusters.entries()) {
|
|
283
|
+
const width = cluster.end - cluster.start;
|
|
284
|
+
const height = rowSpan.end - rowSpan.start;
|
|
285
|
+
const coverage = (width * height) / Math.max(1, raw.width * raw.height);
|
|
286
|
+
if (coverage < 0.025 || coverage > 0.98) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const tall = height / Math.max(1, width) >= 1.35;
|
|
290
|
+
candidates.push(buildRegionFromPixels(tall ? `borehole-log-panel-candidate-${index + 1}` : `table-log-panel-candidate-${index + 2}`, tall ? 'detected borehole/log strip candidate' : 'detected table/log panel candidate', cluster.start, rowSpan.start, cluster.end, rowSpan.end, raw.width, raw.height, 10));
|
|
291
|
+
}
|
|
292
|
+
const regionV2Candidates = options.regionV2 ? buildRegionV2PanelCandidates(raw, rowSpan) : [];
|
|
293
|
+
const candidatePool = options.regionV2
|
|
294
|
+
? [...regionV2Candidates, full, ...candidates.slice(1)]
|
|
295
|
+
: candidates;
|
|
296
|
+
const deduped = [];
|
|
297
|
+
for (const candidate of candidatePool) {
|
|
298
|
+
const coverage = candidate.coverageRatio ?? 0;
|
|
299
|
+
const maxCoverage = options.regionV2 && candidate.id === 'region-v2-table-panel-overview' ? 1 : 0.98;
|
|
300
|
+
if (coverage < 0.025 || coverage > maxCoverage) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (!deduped.some((existing) => regionOverlap(existing, candidate) > 0.88)) {
|
|
304
|
+
deduped.push(candidate);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return deduped.slice(0, options.regionV2 ? 8 : 5);
|
|
308
|
+
}
|
|
309
|
+
function buildRegionV2PanelCandidates(raw, rowSpan) {
|
|
310
|
+
const activeColThreshold = Math.max(8, Math.round(raw.height * 0.012));
|
|
311
|
+
const activeCols = raw.colDarkCounts
|
|
312
|
+
.map((count, index) => ({ count, index }))
|
|
313
|
+
.filter((col) => col.count >= activeColThreshold)
|
|
314
|
+
.map((col) => col.index);
|
|
315
|
+
const colClusters = mergeLineClusters(groupContiguous(activeCols, Math.max(4, Math.round(raw.width * 0.01))), Math.max(12, Math.round(raw.width * 0.035))).filter((cluster) => {
|
|
316
|
+
const width = cluster.end - cluster.start + 1;
|
|
317
|
+
return width >= raw.width * 0.055 && width <= raw.width * 0.58;
|
|
318
|
+
});
|
|
319
|
+
const candidates = [];
|
|
320
|
+
const broadCoverage = ((raw.maxX - raw.minX + 1) * (rowSpan.end - rowSpan.start + 1)) / Math.max(1, raw.width * raw.height);
|
|
321
|
+
if (broadCoverage >= 0.02) {
|
|
322
|
+
candidates.push(buildRegionFromPixels('region-v2-table-panel-overview', 'region-v2 table panel crop', raw.minX, rowSpan.start, raw.maxX, rowSpan.end, raw.width, raw.height, 14));
|
|
323
|
+
}
|
|
324
|
+
for (const [index, cluster] of colClusters.entries()) {
|
|
325
|
+
const localBounds = boundsForColumnRange(raw, cluster.start, cluster.end, rowSpan);
|
|
326
|
+
if (!localBounds) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const width = localBounds.maxX - localBounds.minX + 1;
|
|
330
|
+
const height = localBounds.maxY - localBounds.minY + 1;
|
|
331
|
+
const coverage = (width * height) / Math.max(1, raw.width * raw.height);
|
|
332
|
+
if (coverage < 0.02 || coverage > 0.92) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const tall = height / Math.max(1, width) >= 1.22;
|
|
336
|
+
const label = tall
|
|
337
|
+
? 'region-v2 borehole/log strip crop'
|
|
338
|
+
: 'region-v2 table panel crop';
|
|
339
|
+
candidates.push(buildRegionFromPixels(tall ? `region-v2-borehole-log-strip-${index + 1}` : `region-v2-table-panel-${index + 1}`, label, localBounds.minX, localBounds.minY, localBounds.maxX, localBounds.maxY, raw.width, raw.height, 14));
|
|
340
|
+
}
|
|
341
|
+
return candidates;
|
|
342
|
+
}
|
|
343
|
+
function boundsForColumnRange(raw, minCol, maxCol, rowSpan) {
|
|
344
|
+
let minX = raw.width;
|
|
345
|
+
let minY = raw.height;
|
|
346
|
+
let maxX = -1;
|
|
347
|
+
let maxY = -1;
|
|
348
|
+
const left = Math.max(0, minCol);
|
|
349
|
+
const right = Math.min(raw.width - 1, maxCol);
|
|
350
|
+
const top = Math.max(0, rowSpan.start);
|
|
351
|
+
const bottom = Math.min(raw.height - 1, rowSpan.end);
|
|
352
|
+
for (let y = top; y <= bottom; y += 1) {
|
|
353
|
+
for (let x = left; x <= right; x += 1) {
|
|
354
|
+
const value = raw.data[(y * raw.width + x) * raw.channels] ?? 255;
|
|
355
|
+
if (value < 238) {
|
|
356
|
+
if (x < minX)
|
|
357
|
+
minX = x;
|
|
358
|
+
if (x > maxX)
|
|
359
|
+
maxX = x;
|
|
360
|
+
if (y < minY)
|
|
361
|
+
minY = y;
|
|
362
|
+
if (y > maxY)
|
|
363
|
+
maxY = y;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return maxX > minX && maxY > minY ? { minX, minY, maxX, maxY } : null;
|
|
368
|
+
}
|
|
369
|
+
function scorePreprocessRegions(raw, regions) {
|
|
370
|
+
if (!raw) {
|
|
371
|
+
return regions;
|
|
372
|
+
}
|
|
373
|
+
return regions.map((region) => ({
|
|
374
|
+
...region,
|
|
375
|
+
quality: scoreRegionQuality(raw, region),
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
378
|
+
async function attachRegionAssets(sharp, buffer, regions) {
|
|
379
|
+
const metadata = await readImageMetadata(sharp, buffer, 'image/png');
|
|
380
|
+
const width = metadata.width ?? 0;
|
|
381
|
+
const height = metadata.height ?? 0;
|
|
382
|
+
if (width < 16 || height < 16) {
|
|
383
|
+
return regions;
|
|
384
|
+
}
|
|
385
|
+
const cropped = await Promise.all(regions.map(async (region) => {
|
|
386
|
+
if (!region.bbox2d || region.id === 'normalized-full-page') {
|
|
387
|
+
return region;
|
|
388
|
+
}
|
|
389
|
+
const left = Math.max(0, Math.floor(region.bbox2d[0] * width));
|
|
390
|
+
const top = Math.max(0, Math.floor(region.bbox2d[1] * height));
|
|
391
|
+
const right = Math.min(width, Math.ceil(region.bbox2d[2] * width));
|
|
392
|
+
const bottom = Math.min(height, Math.ceil(region.bbox2d[3] * height));
|
|
393
|
+
const cropWidth = Math.max(0, right - left);
|
|
394
|
+
const cropHeight = Math.max(0, bottom - top);
|
|
395
|
+
if (cropWidth < 16 || cropHeight < 16) {
|
|
396
|
+
return region;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const crop = await sharp(buffer, { pages: 1 })
|
|
400
|
+
.extract({
|
|
401
|
+
left,
|
|
402
|
+
top,
|
|
403
|
+
width: cropWidth,
|
|
404
|
+
height: cropHeight,
|
|
405
|
+
})
|
|
406
|
+
.resize({
|
|
407
|
+
width: 1400,
|
|
408
|
+
height: 1400,
|
|
409
|
+
fit: 'inside',
|
|
410
|
+
withoutEnlargement: true,
|
|
411
|
+
})
|
|
412
|
+
.grayscale()
|
|
413
|
+
.normalize()
|
|
414
|
+
.sharpen()
|
|
415
|
+
.png()
|
|
416
|
+
.toBuffer();
|
|
417
|
+
const cropMetadata = await readImageMetadata(sharp, crop, 'image/png');
|
|
418
|
+
return {
|
|
419
|
+
...region,
|
|
420
|
+
asset: {
|
|
421
|
+
mimeType: 'image/png',
|
|
422
|
+
byteLength: crop.length,
|
|
423
|
+
sha256: createHash('sha256').update(crop).digest('hex'),
|
|
424
|
+
...(cropMetadata.width != null ? { width: cropMetadata.width } : {}),
|
|
425
|
+
...(cropMetadata.height != null ? { height: cropMetadata.height } : {}),
|
|
426
|
+
normalized: true,
|
|
427
|
+
dataBase64: crop.toString('base64'),
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return region;
|
|
433
|
+
}
|
|
434
|
+
}));
|
|
435
|
+
return cropped;
|
|
436
|
+
}
|
|
437
|
+
async function scoreImagePreprocessQuality(sharp, buffer, regions, deskew) {
|
|
438
|
+
try {
|
|
439
|
+
const raw = await readRawPreprocessImage(sharp, buffer);
|
|
440
|
+
if (!raw) {
|
|
441
|
+
return {
|
|
442
|
+
score: 0,
|
|
443
|
+
contentCoverageRatio: 0,
|
|
444
|
+
darkPixelRatio: 0,
|
|
445
|
+
regionCoverageRatio: 0,
|
|
446
|
+
regionCount: 0,
|
|
447
|
+
cropAssetCount: 0,
|
|
448
|
+
deskew,
|
|
449
|
+
warnings: ['image-quality-unavailable'],
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const pageArea = Math.max(1, raw.width * raw.height);
|
|
453
|
+
const darkPixelRatio = raw.darkCount / pageArea;
|
|
454
|
+
const contentCoverageRatio = raw.maxX > raw.minX && raw.maxY > raw.minY
|
|
455
|
+
? ((raw.maxX - raw.minX + 1) * (raw.maxY - raw.minY + 1)) / pageArea
|
|
456
|
+
: 0;
|
|
457
|
+
const cropRegions = regions.filter((region) => region.id !== 'normalized-full-page' && region.id !== 'original-full-page');
|
|
458
|
+
const regionCoverageRatio = Math.min(1, cropRegions.reduce((sum, region) => sum + (region.coverageRatio ?? 0), 0));
|
|
459
|
+
const cropAssetCount = cropRegions.filter((region) => region.asset).length;
|
|
460
|
+
const warnings = [
|
|
461
|
+
darkPixelRatio < 0.002 ? 'very-low-content-density' : null,
|
|
462
|
+
contentCoverageRatio < 0.03 ? 'low-page-content-coverage' : null,
|
|
463
|
+
contentCoverageRatio > 0.97 ? 'content-nearly-full-page' : null,
|
|
464
|
+
cropRegions.length === 0 ? 'no-log-or-table-crops-detected' : null,
|
|
465
|
+
cropRegions.some((region) => (region.quality?.score ?? 1) < 0.45) ? 'low-quality-region-crop' : null,
|
|
466
|
+
].filter((value) => value != null);
|
|
467
|
+
let score = 0.45;
|
|
468
|
+
score += Math.min(0.2, darkPixelRatio * 3);
|
|
469
|
+
score += contentCoverageRatio >= 0.03 && contentCoverageRatio <= 0.97 ? 0.12 : 0;
|
|
470
|
+
score += Math.min(0.12, regionCoverageRatio * 0.45);
|
|
471
|
+
score += Math.min(0.08, cropRegions.length * 0.02);
|
|
472
|
+
score += cropAssetCount > 0 ? 0.08 : 0;
|
|
473
|
+
score += deskew.applied ? 0.04 : 0;
|
|
474
|
+
score -= Math.min(0.18, warnings.length * 0.045);
|
|
475
|
+
return {
|
|
476
|
+
score: roundRatio(score),
|
|
477
|
+
contentCoverageRatio: roundRatio(contentCoverageRatio),
|
|
478
|
+
darkPixelRatio: roundRatio(darkPixelRatio),
|
|
479
|
+
regionCoverageRatio: roundRatio(regionCoverageRatio),
|
|
480
|
+
regionCount: cropRegions.length,
|
|
481
|
+
cropAssetCount,
|
|
482
|
+
deskew,
|
|
483
|
+
warnings,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return {
|
|
488
|
+
score: 0,
|
|
489
|
+
contentCoverageRatio: 0,
|
|
490
|
+
darkPixelRatio: 0,
|
|
491
|
+
regionCoverageRatio: 0,
|
|
492
|
+
regionCount: 0,
|
|
493
|
+
cropAssetCount: 0,
|
|
494
|
+
deskew,
|
|
495
|
+
warnings: ['image-quality-scoring-failed'],
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function buildRegionFromPixels(id, label, minX, minY, maxX, maxY, width, height, padding = 4) {
|
|
500
|
+
const left = clampRatio((minX - padding) / width);
|
|
501
|
+
const top = clampRatio((minY - padding) / height);
|
|
502
|
+
const right = clampRatio((maxX + 1 + padding) / width);
|
|
503
|
+
const bottom = clampRatio((maxY + 1 + padding) / height);
|
|
504
|
+
const area = Math.max(0, right - left) * Math.max(0, bottom - top);
|
|
505
|
+
return {
|
|
506
|
+
id,
|
|
507
|
+
source: 'preprocessing',
|
|
508
|
+
label,
|
|
509
|
+
bbox2d: [roundRatio(left), roundRatio(top), roundRatio(right), roundRatio(bottom)],
|
|
510
|
+
coverageRatio: roundRatio(area),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function scoreRegionQuality(raw, region) {
|
|
514
|
+
const bbox = region.bbox2d ?? [0, 0, 1, 1];
|
|
515
|
+
const left = Math.max(0, Math.floor(bbox[0] * raw.width));
|
|
516
|
+
const top = Math.max(0, Math.floor(bbox[1] * raw.height));
|
|
517
|
+
const right = Math.min(raw.width, Math.ceil(bbox[2] * raw.width));
|
|
518
|
+
const bottom = Math.min(raw.height, Math.ceil(bbox[3] * raw.height));
|
|
519
|
+
const width = Math.max(0, right - left);
|
|
520
|
+
const height = Math.max(0, bottom - top);
|
|
521
|
+
const area = Math.max(1, width * height);
|
|
522
|
+
let dark = 0;
|
|
523
|
+
let rowHits = 0;
|
|
524
|
+
let colHits = 0;
|
|
525
|
+
for (let y = top; y < bottom; y += 1) {
|
|
526
|
+
let rowDark = 0;
|
|
527
|
+
for (let x = left; x < right; x += 1) {
|
|
528
|
+
const value = raw.data[(y * raw.width + x) * raw.channels] ?? 255;
|
|
529
|
+
if (value < 238) {
|
|
530
|
+
dark += 1;
|
|
531
|
+
rowDark += 1;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (width > 0 && rowDark / width >= 0.18) {
|
|
535
|
+
rowHits += 1;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
for (let x = left; x < right; x += 1) {
|
|
539
|
+
let colDark = 0;
|
|
540
|
+
for (let y = top; y < bottom; y += 1) {
|
|
541
|
+
const value = raw.data[(y * raw.width + x) * raw.channels] ?? 255;
|
|
542
|
+
if (value < 238) {
|
|
543
|
+
colDark += 1;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (height > 0 && colDark / height >= 0.12) {
|
|
547
|
+
colHits += 1;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const darkPixelRatio = dark / area;
|
|
551
|
+
const lineDensity = Math.min(1, (rowHits + colHits) / Math.max(1, width + height));
|
|
552
|
+
const coverageRatio = Math.max(0, Math.min(1, region.coverageRatio ?? area / Math.max(1, raw.width * raw.height)));
|
|
553
|
+
const warnings = [
|
|
554
|
+
darkPixelRatio < 0.002 ? 'low-region-content-density' : null,
|
|
555
|
+
coverageRatio < 0.02 ? 'small-region-coverage' : null,
|
|
556
|
+
coverageRatio > 0.98 ? 'region-covers-full-page' : null,
|
|
557
|
+
region.id.includes('table') || region.id.includes('log')
|
|
558
|
+
? lineDensity < 0.004 ? 'weak-table-line-structure' : null
|
|
559
|
+
: null,
|
|
560
|
+
].filter((value) => value != null);
|
|
561
|
+
let score = 0.35;
|
|
562
|
+
score += Math.min(0.25, darkPixelRatio * 4);
|
|
563
|
+
score += Math.min(0.25, lineDensity * 35);
|
|
564
|
+
score += coverageRatio >= 0.02 && coverageRatio <= 0.95 ? 0.12 : 0;
|
|
565
|
+
score -= Math.min(0.16, warnings.length * 0.04);
|
|
566
|
+
return {
|
|
567
|
+
score: roundRatio(score),
|
|
568
|
+
darkPixelRatio: roundRatio(darkPixelRatio),
|
|
569
|
+
lineDensity: roundRatio(lineDensity),
|
|
570
|
+
coverageRatio: roundRatio(coverageRatio),
|
|
571
|
+
warnings,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function groupContiguous(values, maxGap = 1) {
|
|
575
|
+
if (values.length === 0) {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
const sorted = [...new Set(values)].sort((left, right) => left - right);
|
|
579
|
+
const groups = [];
|
|
580
|
+
let start = sorted[0];
|
|
581
|
+
let end = start;
|
|
582
|
+
let count = 1;
|
|
583
|
+
for (const value of sorted.slice(1)) {
|
|
584
|
+
if (value - end <= maxGap + 1) {
|
|
585
|
+
end = value;
|
|
586
|
+
count += 1;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
groups.push({ start, end, count });
|
|
590
|
+
start = value;
|
|
591
|
+
end = value;
|
|
592
|
+
count = 1;
|
|
593
|
+
}
|
|
594
|
+
groups.push({ start, end, count });
|
|
595
|
+
return groups;
|
|
596
|
+
}
|
|
597
|
+
function mergeLineClusters(groups, maxGap) {
|
|
598
|
+
if (groups.length === 0) {
|
|
599
|
+
return [];
|
|
600
|
+
}
|
|
601
|
+
const merged = [];
|
|
602
|
+
let current = { ...groups[0] };
|
|
603
|
+
for (const group of groups.slice(1)) {
|
|
604
|
+
if (group.start - current.end <= maxGap) {
|
|
605
|
+
current = {
|
|
606
|
+
start: current.start,
|
|
607
|
+
end: group.end,
|
|
608
|
+
count: current.count + group.count,
|
|
609
|
+
};
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
merged.push(current);
|
|
613
|
+
current = { ...group };
|
|
614
|
+
}
|
|
615
|
+
merged.push(current);
|
|
616
|
+
return merged;
|
|
617
|
+
}
|
|
618
|
+
function regionOverlap(left, right) {
|
|
619
|
+
if (!left.bbox2d || !right.bbox2d) {
|
|
620
|
+
return 0;
|
|
621
|
+
}
|
|
622
|
+
const x1 = Math.max(left.bbox2d[0], right.bbox2d[0]);
|
|
623
|
+
const y1 = Math.max(left.bbox2d[1], right.bbox2d[1]);
|
|
624
|
+
const x2 = Math.min(left.bbox2d[2], right.bbox2d[2]);
|
|
625
|
+
const y2 = Math.min(left.bbox2d[3], right.bbox2d[3]);
|
|
626
|
+
const intersection = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
|
|
627
|
+
const leftArea = Math.max(0, left.bbox2d[2] - left.bbox2d[0]) * Math.max(0, left.bbox2d[3] - left.bbox2d[1]);
|
|
628
|
+
const rightArea = Math.max(0, right.bbox2d[2] - right.bbox2d[0]) * Math.max(0, right.bbox2d[3] - right.bbox2d[1]);
|
|
629
|
+
const union = leftArea + rightArea - intersection;
|
|
630
|
+
return union > 0 ? intersection / union : 0;
|
|
631
|
+
}
|
|
632
|
+
function clampRatio(value) {
|
|
633
|
+
return Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0));
|
|
634
|
+
}
|
|
635
|
+
function roundRatio(value) {
|
|
636
|
+
return Math.round(value * 1000) / 1000;
|
|
637
|
+
}
|
|
638
|
+
function roundAngle(value) {
|
|
639
|
+
return Math.round(value * 100) / 100;
|
|
640
|
+
}
|
|
19
641
|
async function loadPdfRendererModule() {
|
|
20
642
|
if (!cachedPdfRendererUrl) {
|
|
21
643
|
const pdfjsPackagePath = require.resolve('pdfjs-dist/package.json');
|
|
@@ -56,35 +678,82 @@ export async function encodeRawRasterToPng(pixels, details) {
|
|
|
56
678
|
.png()
|
|
57
679
|
.toBuffer();
|
|
58
680
|
}
|
|
59
|
-
export async function preprocessVisionImageBuffer(buffer, mimeType, policy =
|
|
681
|
+
export async function preprocessVisionImageBuffer(buffer, mimeType, policy = resolveVisionImagePreprocessPolicy()) {
|
|
60
682
|
if (policy === 'none') {
|
|
683
|
+
const input = await readImageMetadata(null, buffer, mimeType);
|
|
684
|
+
const metadata = buildPreprocessingMetadata({
|
|
685
|
+
policy,
|
|
686
|
+
transformed: false,
|
|
687
|
+
input,
|
|
688
|
+
output: input,
|
|
689
|
+
operations: [],
|
|
690
|
+
warnings: [],
|
|
691
|
+
});
|
|
61
692
|
return {
|
|
62
693
|
buffer: Buffer.from(buffer),
|
|
63
694
|
mimeType,
|
|
64
695
|
transformed: false,
|
|
65
696
|
warnings: [],
|
|
697
|
+
preprocessing: metadata,
|
|
66
698
|
};
|
|
67
699
|
}
|
|
68
700
|
const sharp = await loadSharp();
|
|
701
|
+
const input = await readImageMetadata(sharp, buffer, mimeType);
|
|
69
702
|
if (!sharp) {
|
|
703
|
+
const warnings = ['Image preprocessing skipped because sharp is unavailable in this runtime.'];
|
|
70
704
|
return {
|
|
71
705
|
buffer: Buffer.from(buffer),
|
|
72
706
|
mimeType,
|
|
73
707
|
transformed: false,
|
|
74
|
-
warnings
|
|
708
|
+
warnings,
|
|
709
|
+
preprocessing: buildPreprocessingMetadata({
|
|
710
|
+
policy,
|
|
711
|
+
transformed: false,
|
|
712
|
+
input,
|
|
713
|
+
output: input,
|
|
714
|
+
operations: [],
|
|
715
|
+
warnings,
|
|
716
|
+
}),
|
|
75
717
|
};
|
|
76
718
|
}
|
|
77
719
|
try {
|
|
78
|
-
|
|
720
|
+
let working = await sharp(Buffer.from(buffer), { pages: 1 })
|
|
79
721
|
.rotate()
|
|
80
722
|
.flatten({ background: '#ffffff' })
|
|
723
|
+
.png()
|
|
724
|
+
.toBuffer();
|
|
725
|
+
const operations = [
|
|
726
|
+
'auto-orient',
|
|
727
|
+
'flatten-white-background',
|
|
728
|
+
];
|
|
729
|
+
const regionV2 = policy === 'region-v2';
|
|
730
|
+
const deskew = await estimateDeskewCorrection(sharp, working, { finePass: regionV2 });
|
|
731
|
+
if (deskew.applied) {
|
|
732
|
+
working = await sharp(working, { pages: 1 })
|
|
733
|
+
.rotate(deskew.angleDeg, { background: '#ffffff' })
|
|
734
|
+
.flatten({ background: '#ffffff' })
|
|
735
|
+
.png()
|
|
736
|
+
.toBuffer();
|
|
737
|
+
operations.push(`deskew-angle-${deskew.angleDeg.toFixed(2)}deg`);
|
|
738
|
+
if (regionV2) {
|
|
739
|
+
operations.push('projection-profile-fine-deskew');
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
operations.push('deskew-not-applied');
|
|
744
|
+
if (regionV2) {
|
|
745
|
+
operations.push('projection-profile-fine-deskew-not-applied');
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const normalizedMaxDimension = regionV2 ? 2200 : 1800;
|
|
749
|
+
const output = await sharp(working, { pages: 1 })
|
|
81
750
|
.trim({
|
|
82
751
|
background: '#ffffff',
|
|
83
752
|
threshold: 10,
|
|
84
753
|
})
|
|
85
754
|
.resize({
|
|
86
|
-
width:
|
|
87
|
-
height:
|
|
755
|
+
width: normalizedMaxDimension,
|
|
756
|
+
height: normalizedMaxDimension,
|
|
88
757
|
fit: 'inside',
|
|
89
758
|
withoutEnlargement: true,
|
|
90
759
|
})
|
|
@@ -93,21 +762,70 @@ export async function preprocessVisionImageBuffer(buffer, mimeType, policy = 'oc
|
|
|
93
762
|
.sharpen()
|
|
94
763
|
.png()
|
|
95
764
|
.toBuffer();
|
|
765
|
+
const outputMetadata = await readImageMetadata(sharp, output, 'image/png');
|
|
766
|
+
operations.push('trim-white-margins-threshold-10', `resize-inside-${normalizedMaxDimension}-no-enlarge`, 'grayscale', 'normalize-contrast', 'sharpen', 'encode-png');
|
|
767
|
+
let regions = await detectPreprocessingRegions(sharp, output, policy);
|
|
768
|
+
if (regions.some((region) => region.id === 'content-bounding-box')) {
|
|
769
|
+
operations.push('detect-content-bounding-box');
|
|
770
|
+
}
|
|
771
|
+
if (regions.some((region) => region.id.startsWith('table-log-panel-candidate'))) {
|
|
772
|
+
operations.push('detect-table-log-panel-candidate');
|
|
773
|
+
}
|
|
774
|
+
if (regions.some((region) => region.id.startsWith('borehole-log-panel-candidate'))) {
|
|
775
|
+
operations.push('detect-borehole-log-panel-candidate');
|
|
776
|
+
}
|
|
777
|
+
if (regions.some((region) => region.id.startsWith('region-v2-table-panel'))) {
|
|
778
|
+
operations.push('detect-region-v2-table-panel');
|
|
779
|
+
}
|
|
780
|
+
if (regions.some((region) => region.id.startsWith('region-v2-borehole-log-strip'))) {
|
|
781
|
+
operations.push('detect-region-v2-borehole-log-strip');
|
|
782
|
+
}
|
|
783
|
+
if (regions.filter((region) => region.id.includes('panel-candidate')).length > 1) {
|
|
784
|
+
operations.push('detect-multiple-table-log-candidates');
|
|
785
|
+
}
|
|
786
|
+
regions = await attachRegionAssets(sharp, output, regions);
|
|
787
|
+
if (regions.some((region) => region.asset)) {
|
|
788
|
+
operations.push('persist-region-crop-assets');
|
|
789
|
+
operations.push('normalize-region-crop-assets');
|
|
790
|
+
}
|
|
791
|
+
if (regions.some((region) => region.quality)) {
|
|
792
|
+
operations.push('score-preprocessing-regions');
|
|
793
|
+
}
|
|
794
|
+
const quality = await scoreImagePreprocessQuality(sharp, output, regions, deskew);
|
|
96
795
|
return {
|
|
97
796
|
buffer: output,
|
|
98
797
|
mimeType: 'image/png',
|
|
99
798
|
transformed: true,
|
|
100
799
|
warnings: [],
|
|
800
|
+
preprocessing: buildPreprocessingMetadata({
|
|
801
|
+
policy,
|
|
802
|
+
transformed: true,
|
|
803
|
+
input,
|
|
804
|
+
output: outputMetadata,
|
|
805
|
+
operations,
|
|
806
|
+
regions,
|
|
807
|
+
quality,
|
|
808
|
+
warnings: [],
|
|
809
|
+
}),
|
|
101
810
|
};
|
|
102
811
|
}
|
|
103
812
|
catch (error) {
|
|
813
|
+
const warnings = [
|
|
814
|
+
`Image preprocessing skipped: ${error instanceof Error ? error.message : String(error)}`,
|
|
815
|
+
];
|
|
104
816
|
return {
|
|
105
817
|
buffer: Buffer.from(buffer),
|
|
106
818
|
mimeType,
|
|
107
819
|
transformed: false,
|
|
108
|
-
warnings
|
|
109
|
-
|
|
110
|
-
|
|
820
|
+
warnings,
|
|
821
|
+
preprocessing: buildPreprocessingMetadata({
|
|
822
|
+
policy,
|
|
823
|
+
transformed: false,
|
|
824
|
+
input,
|
|
825
|
+
output: input,
|
|
826
|
+
operations: [],
|
|
827
|
+
warnings,
|
|
828
|
+
}),
|
|
111
829
|
};
|
|
112
830
|
}
|
|
113
831
|
}
|
|
@@ -137,13 +855,14 @@ export async function renderPdfPageToImageBuffer(buffer, pageNumber = 1, options
|
|
|
137
855
|
viewport,
|
|
138
856
|
}).promise;
|
|
139
857
|
const renderedBuffer = canvas.toBuffer('image/png');
|
|
140
|
-
const preprocessed = await preprocessVisionImageBuffer(renderedBuffer, 'image/png', options?.preprocessPolicy ??
|
|
858
|
+
const preprocessed = await preprocessVisionImageBuffer(renderedBuffer, 'image/png', options?.preprocessPolicy ?? resolveVisionImagePreprocessPolicy());
|
|
141
859
|
return {
|
|
142
860
|
buffer: preprocessed.buffer,
|
|
143
861
|
mimeType: 'image/png',
|
|
144
862
|
width: canvas.width,
|
|
145
863
|
height: canvas.height,
|
|
146
864
|
warnings: preprocessed.warnings,
|
|
865
|
+
preprocessing: preprocessed.preprocessing,
|
|
147
866
|
};
|
|
148
867
|
}
|
|
149
868
|
finally {
|