mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Round-2 sim fixes: one plate rule, plate size from the gate's floor, missing means empty
plateVerdict() is shared by the plates gate and generate-image's PLATE-WARN so they cannot disagree; --plate picks a frame that clears the 1.5x width floor (a square region wider than 682px takes the 1536 landscape frame); 'missing' on text/chrome/control regions requires the build region to be near-empty, so a 12px rule a few pixels off reads as contradicted or drift, not missing; the responsive gate does not re-litigate a plate that passed the hero; record hero after close does not inflate the attempt count. AI-assisted (Claude). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
Abdul Wahab
co-authored by
Claude
parent
af4ac68805
commit
bfa6014796
@@ -195,6 +195,25 @@ export function gateSpec(state, { specPath = SPEC_PATH } = {}) {
|
||||
return { ok: true, reasons: [], summary: `${spec.regions.length} regions, ${plates} plates` };
|
||||
}
|
||||
|
||||
/**
|
||||
* The one plate rule, shared by the plates gate and generate-image's
|
||||
* PLATE-WARN so they never disagree. `score` is compare().whole against the
|
||||
* masked comp crop under cover alignment.
|
||||
*/
|
||||
export function plateVerdict(region, score) {
|
||||
const isTexture = region.kind === 'texture';
|
||||
const reasons = [];
|
||||
if (isTexture) {
|
||||
const effective = 0.5 * score.color + 0.5 * Math.min(1, score.detail / 0.6);
|
||||
if (effective < PLATE_MIN) reasons.push(`scores ${(effective * 100).toFixed(0)}% as the material of region ${region.id} (color ${(score.color * 100).toFixed(0)}%, detail ${(score.detail * 100).toFixed(0)}%); crop a clean patch of the comp region (comp-spec.mjs --crop ${region.id} --raw) and mirror-tile it, generate only when no clean patch exists`);
|
||||
return { ok: reasons.length === 0, reasons, effective };
|
||||
}
|
||||
if (score.detailAdded > 0.45) reasons.push(`carries detail the comp region ${region.id} does not have (added-detail ${(score.detailAdded * 100).toFixed(0)}% of cells): noise, grain, or a busier subject where the comp is calm; regenerate from the crop reference without adding texture`);
|
||||
if (score.structure < PLATE_STRUCTURE_MIN) reasons.push(`structure ${(score.structure * 100).toFixed(0)}% against the comp region ${region.id}: the composition of the plate is not the region's (different subject, orientation, or crop); regenerate with comp-spec.mjs --crop ${region.id} as the reference image`);
|
||||
if (score.overall < PLATE_MIN) reasons.push(`scores ${(score.overall * 100).toFixed(0)}% against the comp region ${region.id} (structure ${(score.structure * 100).toFixed(0)}%, color ${(score.color * 100).toFixed(0)}%, detail ${(score.detail * 100).toFixed(0)}%); regenerate with the crop as --ref and the comp-spec plate prompt`);
|
||||
return { ok: reasons.length === 0, reasons, effective: score.overall };
|
||||
}
|
||||
|
||||
export function gatePlates(state, { specPath = SPEC_PATH } = {}) {
|
||||
const spec = loadSpec(specPath);
|
||||
if (!spec) return { ok: false, reasons: ['no spec'] };
|
||||
@@ -221,10 +240,8 @@ export function gatePlates(state, { specPath = SPEC_PATH } = {}) {
|
||||
const ref = plateReference(comp, spec, r);
|
||||
const res = compare({ comp: ref, build: img, align: 'cover', spec: null, kind: r.kind });
|
||||
score = res.whole;
|
||||
const effective = isTexture ? 0.5 * score.color + 0.5 * Math.min(1, score.detail / 0.6) : score.overall;
|
||||
if (!isTexture && score.detailAdded > 0.45) reasons.push(`plate ${file} carries detail the comp region ${r.id} does not have (added-detail ${(score.detailAdded * 100).toFixed(0)}% of cells): noise, grain, or a busier subject where the comp is calm. Regenerate from the crop reference; do not add texture the comp does not show.`);
|
||||
if (!isTexture && score.structure < PLATE_STRUCTURE_MIN) reasons.push(`plate ${file} has structure ${(score.structure * 100).toFixed(0)}% against the comp region ${r.id}: the composition of the plate is not the region's (a different subject, orientation, or crop). Regenerate with comp-spec.mjs --crop ${r.id} as the reference image; a plate that only shares the palette and busyness is not this plate.`);
|
||||
if (effective < PLATE_MIN) reasons.push(`plate ${file} scores ${(effective * 100).toFixed(0)}% against the comp region ${r.id} (structure ${(score.structure * 100).toFixed(0)}%, color ${(score.color * 100).toFixed(0)}%, detail ${(score.detail * 100).toFixed(0)}%); it does not read as the same ${isTexture ? 'material' : 'region'}. Regenerate with the crop as --ref and the comp-spec plate prompt${isTexture ? ', or crop a clean patch of the comp region and tile it' : ''}.`);
|
||||
const v = plateVerdict(r, score);
|
||||
for (const reason of v.reasons) reasons.push(`plate ${file}: ${reason}`);
|
||||
}
|
||||
plates.push({ id: r.id, file, status: 'ok', size: `${img.width}x${img.height}`, score: score ? score.overall : null });
|
||||
}
|
||||
@@ -401,7 +418,10 @@ export function gateResponsive(state, { specPath = SPEC_PATH, min = RESPONSIVE_M
|
||||
let report;
|
||||
try { report = JSON.parse(res.stdout); } catch { return { ok: false, reasons: [`comp-diff failed on ${desktop}: ${res.stderr || res.stdout}`] }; }
|
||||
const missing = report.regions.filter((r) => r.verdict === 'missing');
|
||||
const contradictedDirection = report.regions.filter((r) => r.verdict === 'contradicted' && (r.kind === 'plate' || r.kind === 'image' || r.kind === 'text'));
|
||||
// A plate placed and passed at the hero is not re-litigated at 1440: the
|
||||
// rescale alone drops SSIM on a busy region. Text can still contradict
|
||||
// (a wrapped headline is a different composition).
|
||||
const contradictedDirection = report.regions.filter((r) => r.verdict === 'contradicted' && r.kind === 'text');
|
||||
if (report.overall < min) reasons.push(`the desktop capture (${report.buildSize}) scores ${(report.overall * 100).toFixed(0)}% against the comp, under ${(min * 100).toFixed(0)}%: the first viewport does not survive a common desktop width. The hero passed at ${state.breakpoint || 'the comp size'}; the layout must hold from ~1280 up, not only at the comp's exact width (grid columns in fr / minmax, not fixed px that overflow and wrap).`);
|
||||
for (const r of missing) reasons.push(`at desktop width, region ${r.id} is missing`);
|
||||
for (const r of contradictedDirection) reasons.push(`at desktop width, region ${r.id} (${r.kind}) is contradicted (structure ${(r.score.structure * 100).toFixed(0)}%)`);
|
||||
@@ -542,7 +562,7 @@ async function main() {
|
||||
const which = process.argv[3];
|
||||
if (which !== 'hero') { console.error('build-phase: record hero --build <png>'); process.exit(1); }
|
||||
const gate = gateHero(state, { buildPath: arg('build', HERO_REPRO), min: arg('min') ? parseFloat(arg('min')) : HERO_MIN });
|
||||
state.phases.hero.attempts += 1;
|
||||
if (state.phases.hero.status !== 'closed') state.phases.hero.attempts += 1;
|
||||
state.phases.hero.gate = { ...gate, at: now() };
|
||||
saveState(state);
|
||||
// record is the look, advance is the gate: on the plates-only capture,
|
||||
|
||||
@@ -103,6 +103,7 @@ export function scorePair(a, b, kind = null) {
|
||||
colorIntersection: r4(color.intersection),
|
||||
paletteMatch: r4(color.paletteMatch),
|
||||
detail: r4(detail.score),
|
||||
detailRaw: r4(detail.rawScore ?? detail.score),
|
||||
detailAdded: r4(detail.addedFraction),
|
||||
bands: r4(bands),
|
||||
_detail: detail,
|
||||
@@ -133,6 +134,11 @@ export function bestShift(comp, build, workWidth = 256) {
|
||||
export function verdictFor(s, kind = null) {
|
||||
const painted = kind === 'plate' || kind === 'image' || kind === 'texture';
|
||||
if (painted && s.detail < 0.5) return 'missing';
|
||||
// For text, chrome, and controls "missing" means the build has nothing
|
||||
// there, not that a thin strip sits a few pixels off: require the build's
|
||||
// own energy to be near zero relative to the comp (rawScore, before the
|
||||
// added-detail penalty), and drift for a mere misalignment.
|
||||
if (!painted && s.detail < 0.35 && s.structure < 0.6) return (s.detailRaw != null && s.detailRaw < 0.2) ? 'missing' : 'contradicted';
|
||||
if (s.detail < 0.35 && s.structure < 0.6) return 'missing';
|
||||
// Structure is the one thing a wrong-but-busy region cannot fake: noise,
|
||||
// a mirrored crop, a swapped column, a tile shuffle all keep color and
|
||||
|
||||
@@ -222,9 +222,18 @@ if (plateId) {
|
||||
fs.writeFileSync(refPath, encodePng(ref, { text: { 'impeccable:crop-of': `${spec.comp}#${region.id}` } }));
|
||||
const out = arg('out', region.plate);
|
||||
fs.mkdirSync(path.dirname(out), { recursive: true });
|
||||
// closest supported size to the region's aspect; the page crops the rest with object-fit
|
||||
// Closest supported size to the region's aspect; the page crops the rest
|
||||
// with object-fit. The plates gate demands >= 1.5x the region's width
|
||||
// (capped at 1536), so a square region wider than 682px cannot ship from
|
||||
// 1024x1024: take the 1536-wide landscape frame instead and let cover crop.
|
||||
const aspect = region.px.w / region.px.h;
|
||||
const size = arg('size') || (aspect > 1.2 ? '1536x1024' : aspect < 0.83 ? '1024x1536' : '1024x1024');
|
||||
const needW = Math.min(1536, Math.ceil(region.px.w * 1.5));
|
||||
let size = arg('size');
|
||||
if (!size) {
|
||||
if (aspect > 1.2) size = '1536x1024';
|
||||
else if (aspect < 0.83) size = needW > 1024 ? '1536x1024' : '1024x1536';
|
||||
else size = needW > 1024 ? '1536x1024' : '1024x1024';
|
||||
}
|
||||
const extra = arg('prompt') || (arg('prompt-file') ? fs.readFileSync(arg('prompt-file'), 'utf8') : '');
|
||||
const prompt = [platePrompt(spec, region), extra].filter(Boolean).join(' ');
|
||||
plateCtx = { spec, specPath, region, ref, refPath, out, size, prompt, comp, encodePng, resize };
|
||||
@@ -249,8 +258,9 @@ async function scorePlate(ctx, outFile) {
|
||||
const min = arg('min') ? parseFloat(arg('min')) : null;
|
||||
const line = `PLATE-SCORE ${ctx.region.id} ${(s.overall * 100).toFixed(0)}% against the comp region (structure ${(s.structure * 100).toFixed(0)}%, color ${(s.color * 100).toFixed(0)}%, detail ${(s.detail * 100).toFixed(0)}%)`;
|
||||
console.log(line);
|
||||
const bad = s.structure < 0.4 || s.overall < 0.4;
|
||||
if (bad) console.log(`PLATE-WARN the plate does not read as region ${ctx.region.id} (structure ${(s.structure * 100).toFixed(0)}% must be >= 40%, overall >= 40%); open ${outFile} beside ${ctx.refPath} and regenerate with a stricter prompt before building on it. The plates gate will refuse it as it stands.`);
|
||||
const { plateVerdict } = await import('./build-phase.mjs');
|
||||
const v = plateVerdict(ctx.region, s);
|
||||
if (!v.ok) console.log(`PLATE-WARN the plate does not read as region ${ctx.region.id}: ${v.reasons.join('; ')}. Open ${outFile} beside ${ctx.refPath} and regenerate before building on it; the plates gate refuses it as it stands.`);
|
||||
if (min != null && s.overall < min) { console.log(`PLATE-REJECTED below --min ${(min * 100).toFixed(0)}%`); process.exit(3); }
|
||||
} catch (e) {
|
||||
console.log(`PLATE-SCORE unavailable: ${e.message}`);
|
||||
|
||||
Reference in New Issue
Block a user