增加几何校验

This commit is contained in:
Ukawka
2026-09-17 18:50:43 +08:00
parent 4a49fe14e9
commit 1a7b2f14a9
19 changed files with 662 additions and 88 deletions
+36 -10
View File
@@ -3,7 +3,8 @@ import {execFileSync} from 'node:child_process';
import path from 'node:path';
import {projectRoot, publicStage, samInfo} from '../lib/config.mjs';
import {readImage, sha256} from '../lib/images.mjs';
import {partsSchema, parseParts, validateParts} from '../lib/validate-parts.mjs';
import {partsSchema, parseParts, validateParts, upgradeParts} from '../lib/validate-parts.mjs';
import {geometryPython, prepareReference, groundParts} from '../lib/geometry.mjs';
import {annotatedSvg, understandingReport} from '../lib/report.mjs';
import {prepareRun, jsonFile, finishRun} from '../lib/run-artifacts.mjs';
import {analyze, extractVisionText} from '../providers/vision.mjs';
@@ -16,7 +17,8 @@ function codeVersion() {
'stages/model-stage.mjs', 'lib/config.mjs', 'lib/images.mjs', 'lib/report.mjs',
'lib/run-artifacts.mjs', 'lib/validate-parts.mjs', 'providers/http.mjs',
'providers/vision.mjs', 'providers/image-edits.mjs', 'tools/model-check.mjs',
'schemas/semantic-parts.schema.json',
'schemas/semantic-parts.schema.json', 'schemas/semantic-parts-v1.schema.json',
'lib/geometry.mjs', 'lib/python-runtime.mjs', 'geometry_grounding.py', 'normalize_reference.py',
];
const sourceHash = sha256(files.map((file) => `${file}\n${readFileSync(path.join(projectRoot, file), 'utf8')}`).join('\n'));
let commit = null;
@@ -33,37 +35,61 @@ function runIdentity(stage, images, extra) {
};
}
export async function runUnderstanding({loaded, stage, reference, outDir, resume = false}, transport) {
const image = readImage(reference, {pngOnly: true});
const identity = runIdentity(stage, [image], {schema_sha256: sha256(JSON.stringify(partsSchema))});
export async function runUnderstanding({loaded, stage, reference, outDir, resume = false, python, partsFile}, transport) {
const original = readImage(reference, {pngOnly: true});
const importedText = partsFile ? readFileSync(partsFile, 'utf8') : null;
const identity = runIdentity(stage, [original], {schema_sha256: sha256(JSON.stringify(partsSchema)),
input_mode: partsFile ? 'offline_plan' : 'vision_request', imported_plan_sha256: importedText === null ? null : sha256(importedText)});
const run = prepareRun(outDir, sha256(JSON.stringify(identity)), resume);
if (run.reused) return {out: run.out, reused: true};
const manifest = {...identity, fingerprint: sha256(JSON.stringify(identity)), status: 'running', quality_status: 'pending', started_at: new Date().toISOString(), sam: samInfo(loaded)};
jsonFile(run.manifestPath, manifest, stage.apiKey);
writeFileSync(path.join(run.out, 'source.png'), image.bytes);
writeFileSync(path.join(run.out, 'source-original.png'), original.bytes);
writeFileSync(path.join(run.out, 'prompt.txt'), stage.prompt);
jsonFile(path.join(run.out, 'request.json'), {...identity, reference: 'source.png'}, stage.apiKey);
try {
const {payload, metadata} = await analyze(stage, image, partsSchema, transport);
const runtime = geometryPython(python);
const preprocessing = prepareReference(runtime, path.join(run.out, 'source-original.png'), path.join(run.out, 'source.png'));
const image = readImage(path.join(run.out, 'source.png'), {pngOnly: true});
jsonFile(path.join(run.out, 'preprocessing.json'), preprocessing, stage.apiKey);
manifest.prepared_input = {sha256: image.sha256, width: image.width, height: image.height};
jsonFile(path.join(run.out, 'request.json'), {...identity, reference: 'source.png', prepared_input: manifest.prepared_input}, stage.apiKey);
const {payload, metadata} = importedText !== null
? {payload: {imported_plan_text: importedText}, metadata: {source: 'offline_plan', network_requested: false}}
: await analyze(stage, image, partsSchema, transport);
jsonFile(path.join(run.out, 'response.raw.json'), payload, stage.apiKey);
jsonFile(path.join(run.out, 'request-metadata.json'), metadata, stage.apiKey);
let data = null;
let validation;
try {
const text = extractVisionText(payload, stage.protocol);
const text = importedText ?? extractVisionText(payload, stage.protocol);
writeFileSync(path.join(run.out, 'response.txt'), redact(text, stage.apiKey));
data = parseParts(text);
validation = validateParts(data);
if (validation.valid) {
const warnings = validation.warnings;
data = upgradeParts(data);
validation = validateParts(data);
validation.warnings = [...new Set([...warnings, ...validation.warnings])];
}
} catch (error) { validation = {valid: false, errors: [redact(error.message, stage.apiKey)], warnings: []}; }
jsonFile(path.join(run.out, 'validation.json'), validation, stage.apiKey);
writeFileSync(path.join(run.out, 'report.html'), redact(understandingReport(image, data, validation, metadata), stage.apiKey));
if (!validation.valid) throw new Error('结构理解结果未通过本地校验,请查看 validation.json、response.raw.json 和 report.html');
jsonFile(path.join(run.out, 'semantic-parts.json'), data, stage.apiKey);
writeFileSync(path.join(run.out, 'annotated.svg'), redact(annotatedSvg(image, data.parts), stage.apiKey));
const geometry = groundParts(runtime, run.out);
// Geometry failures block individual SAM prompts, while retaining the plan
// and diagnostics for correction. They never fabricate fallback points.
validation.geometry = geometry.summary;
for (const part of geometry.parts) for (const warning of part.warnings) validation.warnings.push(`${part.id}: ${warning}`);
jsonFile(path.join(run.out, 'validation.json'), validation, stage.apiKey);
writeFileSync(path.join(run.out, 'report.html'), redact(understandingReport(image, data, validation, metadata, geometry), stage.apiKey));
writeFileSync(path.join(run.out, 'annotated.svg'), redact(annotatedSvg(image, data.parts, geometry), stage.apiKey));
manifest.request = metadata;
manifest.quality_status = 'needs_review';
manifest.geometry = geometry.summary;
manifest.completed_at = new Date().toISOString();
finishRun(run, manifest, ['source.png', 'prompt.txt', 'request.json', 'response.raw.json', 'response.txt', 'request-metadata.json', 'validation.json', 'report.html', 'semantic-parts.json', 'annotated.svg'], stage.apiKey);
finishRun(run, manifest, ['source-original.png', 'source.png', 'preprocessing.json', 'geometry-validation.json', 'segmentation-prompts.json', 'prompt.txt', 'request.json', 'response.raw.json', 'response.txt', 'request-metadata.json', 'validation.json', 'report.html', 'semantic-parts.json', 'annotated.svg'], stage.apiKey);
return {out: run.out, reused: false, parts: data.parts.length};
} catch (error) {
if (error.diagnostics) jsonFile(path.join(run.out, 'response.error.json'), error.diagnostics, stage.apiKey);