处理尺寸不一致

This commit is contained in:
Ukawka
2026-09-20 22:36:20 +08:00
parent 99d65377ab
commit a00a5621cf
7 changed files with 114 additions and 18 deletions
+25 -9
View File
@@ -24,11 +24,12 @@ function filesIn(dir, prefix = '') {
export async function runCompletion({stage, inputDir, outDir, partIds = [], prepareOnly = false,
candidatesDir, completionSize = 1024, extensionRadius = 32, python: requestedPython,
resume = false, dryRun = false}, transport) {
resume = false, dryRun = false, candidateSizePolicy = 'strict'}, transport) {
if (!inputDir) throw new Error('部件补全需要 --input-dir 指向 SAM 输出目录');
if (!outDir && !dryRun) throw new Error('部件补全需要独立的 --out-dir');
if (prepareOnly && candidatesDir) throw new Error('--prepare-only 与 --candidates-dir 不能同时使用');
const size = Number(completionSize), radius = Number(extensionRadius);
if (!['strict', 'uniform'].includes(candidateSizePolicy)) throw new Error('--candidate-size-policy 仅支持 strict / uniform');
if (!Number.isInteger(size) || size < 128 || size > 4096 || !Number.isInteger(radius) || radius < 1 || radius > 256) {
throw new Error('--completion-size 必须为 128~4096 的整数;--extension-radius 必须为 1~256 的整数');
}
@@ -47,15 +48,24 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
const inputs = Object.fromEntries(inputFiles.map((file) => [file, sha256(readFileSync(path.join(input, file)))]));
const imported = {};
if (candidatesDir) {
const priorManifest = path.resolve(candidatesDir, 'manifest.json');
const previousRun = existsSync(priorManifest) ? readJson(priorManifest) : null;
if (previousRun && (previousRun.output_kind !== 'bounded_part_completion' || previousRun.completion_size !== size || previousRun.extension_radius !== radius ||
Object.entries(inputs).some(([file, hash]) => previousRun.inputs?.[file] !== hash))) {
throw new Error('旧补全目录的输入或画布/延展参数不一致,不能复用其候选');
}
for (const p of plan.parts.filter((p) => p.needs_completion && (!partIds.length || partIds.includes(p.id)))) {
const file = path.resolve(candidatesDir, `${p.id}.png`);
const file = previousRun
? ['png', 'jpg', 'webp'].map((ext) => path.resolve(candidatesDir, 'tasks', p.id, `candidate-1.${ext}`)).find(existsSync)
: path.resolve(candidatesDir, `${p.id}.png`);
if (!file) continue;
if (existsSync(file)) imported[p.id] = {file, sha256: sha256(readFileSync(file))};
}
}
const python = selectPython(requestedPython);
if (dryRun) {
console.log(JSON.stringify({stage: 'completion', input, width: source.width, height: source.height,
selected_parts: partIds, completion_size: size, extension_radius: radius,
selected_parts: partIds, completion_size: size, extension_radius: radius, candidate_size_policy: candidateSizePolicy,
mode: prepareOnly ? 'prepare' : candidatesDir ? 'offline_candidates' : 'image_edit', network_requested: false}, null, 2));
return {dryRun: true};
}
@@ -65,7 +75,7 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
const identity = {schema_version: 1, stage: 'completion', output_kind: 'bounded_part_completion',
mode: prepareOnly ? 'prepare' : candidatesDir ? 'offline_candidates' : 'image_edit',
resolved_stage: publicStage(stage), endpoint_sha256: sha256(stage.baseUrl), prompt_sha256: sha256(stage.prompt),
inputs, part_ids: partIds, completion_size: size, extension_radius: radius,
inputs, part_ids: partIds, completion_size: size, extension_radius: radius, candidate_size_policy: candidateSizePolicy,
imported_candidates: Object.fromEntries(Object.entries(imported).map(([id, value]) => [id, value.sha256])),
code_sha256: sha256(codeFiles.map((file) => `${file}\n${readFileSync(path.join(projectRoot, file), 'utf8')}`).join('\n'))};
const fingerprint = sha256(JSON.stringify(identity));
@@ -75,6 +85,11 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
jsonFile(run.manifestPath, manifest, stage.apiKey);
const result = {schema_version: 1, quality_status: 'needs_review', parts: [], skipped: []};
try {
const requestSize = `${size}x${size}`;
if (!prepareOnly && !candidatesDir && stage.params.size !== undefined && stage.params.size !== requestSize) {
throw new Error(`completion.params.size 必须与局部画布一致:${requestSize}`);
}
const editStage = {...stage, params: {...stage.params, size: requestSize}};
for (const file of inputFiles) {
mkdirSync(path.dirname(path.join(run.out, file)), {recursive: true});
copyFileSync(path.join(input, file), path.join(run.out, file));
@@ -94,17 +109,17 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
let outputs;
if (candidatesDir) {
if (!imported[task.id]) { record.status = 'missing_candidate'; continue; }
outputs = [{bytes: readFileSync(imported[task.id].file), extension: 'png'}];
outputs = [{bytes: readFileSync(imported[task.id].file), extension: path.extname(imported[task.id].file).slice(1)}];
} else {
const images = [readImage(path.join(folder, 'input.png'), {pngOnly: true})];
if (stage.capabilities.multiple_images) images.push(readImage(path.join(folder, 'context.png'), {pngOnly: true}));
const mask = stage.capabilities.mask ? readImage(path.join(folder, 'edit-mask.png'), {pngOnly: true}) : null;
jsonFile(path.join(folder, 'request.json'), {model: stage.model, images: images.map((im) => im.sha256),
jsonFile(path.join(folder, 'request.json'), {model: stage.model, size: requestSize, images: images.map((im) => im.sha256),
edit_mask_sent: Boolean(mask), allowed_mask_sha256: sha256(readFileSync(path.join(folder, 'allowed-mask.png')))}, stage.apiKey);
manifest.network_requested = true;
jsonFile(run.manifestPath, manifest, stage.apiKey);
try {
const {payload, metadata} = await editImage(stage, {images, mask, prompt: fullPrompt}, transport);
const {payload, metadata} = await editImage(editStage, {images, mask, prompt: fullPrompt}, transport);
jsonFile(path.join(folder, 'response.raw.json'), payload, stage.apiKey);
jsonFile(path.join(folder, 'request-metadata.json'), metadata, stage.apiKey);
outputs = await decodeEditedImages(payload, stage, transport);
@@ -119,7 +134,7 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
const filename = `candidate-${i + 1}.${output.extension}`;
writeFileSync(path.join(folder, filename), output.bytes);
const merged = path.join(folder, `merged-${i + 1}.png`), qaFile = path.join(folder, `qa-${i + 1}.json`);
pythonCall(python, ['merge', '--out', folder, '--candidate', path.join(folder, filename), '--merged', merged, '--qa', qaFile]);
pythonCall(python, ['merge', '--out', folder, '--candidate', path.join(folder, filename), '--merged', merged, '--qa', qaFile, '--candidate-size-policy', candidateSizePolicy]);
const qa = readJson(qaFile);
record.candidates.push({file: `tasks/${task.id}/${filename}`, qa});
if (qa.accepted && !record.selected_file) {
@@ -128,7 +143,8 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
}
}
record.status = record.selected_file ? 'geometry_passed_needs_review' : 'rejected';
if (!record.qa) record.qa = {accepted: false, reasons: record.candidates.flatMap((c) => c.qa.reasons)};
if (!record.qa) record.qa = {...record.candidates[0]?.qa, accepted: false,
reasons: [...new Set(record.candidates.flatMap((c) => c.qa.reasons))]};
jsonFile(path.join(run.out, 'completion-result.json'), result, stage.apiKey);
}
manifest.quality_status = prepareOnly ? 'prepared_needs_review' : 'needs_review';