新方案v1角色结构理解

This commit is contained in:
Ukawka
2026-09-17 17:44:24 +08:00
parent 2e79bc4a12
commit 30e82af4f1
21 changed files with 1287 additions and 14 deletions
+118
View File
@@ -0,0 +1,118 @@
import {readFileSync, writeFileSync} from 'node:fs';
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 {annotatedSvg, understandingReport} from '../lib/report.mjs';
import {prepareRun, jsonFile, finishRun} from '../lib/run-artifacts.mjs';
import {analyze, extractVisionText} from '../providers/vision.mjs';
import {editImage, validateEditInputs, decodeEditedImages} from '../providers/image-edits.mjs';
import {redact} from '../providers/http.mjs';
function codeVersion() {
// Include uncommitted source changes as well as a commit ID for experiments.
const files = [
'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',
];
const sourceHash = sha256(files.map((file) => `${file}\n${readFileSync(path.join(projectRoot, file), 'utf8')}`).join('\n'));
let commit = null;
try { commit = execFileSync('git', ['rev-parse', 'HEAD'], {cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore']}).trim(); } catch { /* Not all distributions are Git checkouts. */ }
return {commit, source_sha256: sourceHash};
}
function runIdentity(stage, images, extra) {
return {
schema_version: 1, stage: stage.name, resolved_stage: publicStage(stage),
endpoint_sha256: sha256(stage.baseUrl), prompt_sha256: sha256(stage.prompt),
inputs: images.map((image) => ({sha256: image.sha256, mime: image.mime, width: image.width ?? null, height: image.height ?? null})),
code: codeVersion(), ...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))});
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, '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);
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);
writeFileSync(path.join(run.out, 'response.txt'), redact(text, stage.apiKey));
data = parseParts(text);
validation = validateParts(data);
} 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));
manifest.request = metadata;
manifest.quality_status = 'needs_review';
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);
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);
manifest.status = 'failed';
manifest.quality_status = 'failed';
manifest.error = redact(error.message, stage.apiKey);
jsonFile(run.manifestPath, manifest, stage.apiKey);
throw error;
}
}
export async function runCompletionProbe({stage, imagePaths, maskPath, promptFile, outDir, resume = false}, transport) {
const images = imagePaths.map((file) => readImage(file, {pngOnly: true}));
const mask = maskPath ? readImage(maskPath, {pngOnly: true}) : null;
validateEditInputs(stage, images, mask);
const taskPrompt = promptFile ? readFileSync(promptFile, 'utf8') : '';
if (!taskPrompt.trim()) throw new Error('图片编辑测试需要 --prompt-file 明确指定本次部件和补全需求');
const prompt = `${stage.prompt}\n\nTask:\n${taskPrompt}`;
const identity = runIdentity(stage, images, {task_prompt_sha256: sha256(taskPrompt), mask_sha256: mask?.sha256 || null});
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(), output_kind: 'raw_completion_probe'};
jsonFile(run.manifestPath, manifest, stage.apiKey);
const artifacts = [];
images.forEach((image, i) => { const name = `input-${i}.png`; writeFileSync(path.join(run.out, name), image.bytes); artifacts.push(name); });
if (mask) { writeFileSync(path.join(run.out, 'edit-mask.png'), mask.bytes); artifacts.push('edit-mask.png'); }
writeFileSync(path.join(run.out, 'prompt.txt'), prompt);
jsonFile(path.join(run.out, 'request.json'), identity, stage.apiKey);
try {
const {payload, metadata} = await editImage(stage, {images, mask, prompt}, transport);
jsonFile(path.join(run.out, 'response.raw.json'), payload, stage.apiKey);
jsonFile(path.join(run.out, 'request-metadata.json'), metadata, stage.apiKey);
const outputs = await decodeEditedImages(payload, stage, transport);
manifest.outputs = outputs.map((output, index) => {
const file = `candidate-${index + 1}.${output.extension}`;
writeFileSync(path.join(run.out, file), output.bytes);
artifacts.push(file);
return {file, width: output.width ?? null, height: output.height ?? null, canvas_size_matches: output.width ? output.width === images[0].width && output.height === images[0].height : null};
});
manifest.request = metadata;
manifest.quality_status = 'needs_review';
manifest.notes = ['仅验证图片编辑适配器;尚未执行掩码提取、可见像素恢复、几何验收或 Spine 导出。', 'mask/多图能力由显式配置声明,不能仅凭 API 返回 200 判断模型遵循约束。'];
manifest.completed_at = new Date().toISOString();
finishRun(run, manifest, [...artifacts, 'prompt.txt', 'request.json', 'response.raw.json', 'request-metadata.json'], stage.apiKey);
return {out: run.out, reused: false, candidates: outputs.length};
} catch (error) {
if (error.diagnostics) jsonFile(path.join(run.out, 'response.error.json'), error.diagnostics, stage.apiKey);
manifest.status = 'failed'; manifest.error = redact(error.message, stage.apiKey);
jsonFile(run.manifestPath, manifest, stage.apiKey);
throw error;
}
}