处理尺寸不一致
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user