新方案v1角色结构理解
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user