Files
ComfyAssets-ComfyUI_PromptM…/web/js/workspace/local-parameters.mjs
T
Vito Sansevero 564de9e3f1 feat(inspect): local candidate selection matching the inspector
The browser port of the inspection resolver now answers every record set
the server does, so no path is left to the server any more:

- several sampler candidates, with the identical-branch rule (#270): a
  compare grid whose branches all resolve to one pair (text and recipe)
  is resolved, any other mix is ambiguous;
- stages whose outputs reach only previews are dropped first;
- detailer stages are judged with the samplers in their image ancestry
  (multistage-differs, cycles, depth and the per-stage visit budgets),
  and a sampler that only feeds detailers is not a candidate of its own;
- A1111 `parameters`, with Python's multiline anchors, `\s` and Unicode
  `\d`, the bare footer and Template trailers, counted in code points.

The parity corpus gains the selection cases and a seeded fuzzer, and
every inspection vector now gives the server's prompts, resolution and
facts locally. 16 of 17 mutations are caught; the uncaught one (not
flattening an ambiguous upstream stage) cannot change any answer.
2026-10-22 17:43:33 -07:00

56 lines
3.4 KiB
JavaScript

// A1111 `parameters` for local-resolver.mjs (#249 local Inspect, PR L3 Task 9): a port of
// utils/prompt_pair_resolver.py `_parameters`, `_footer` and `_trailer_lines` for the inspection profile.
// Python's `re` differs from JavaScript's in three ways that matter here, so the patterns spell them out:
// multiline `^`/`$` break only at "\n" (not "\r", U+2028 or U+2029), `\s` is the `str.isspace()` set and `\d`
// is any Unicode decimal digit (Nd). Lengths count code points. No DOM.
import { PY_SPACE, pyLen, pyStrip } from './py-json.mjs';
import { EMPTY_POSITIVE_REASON, result } from './local-resolver-support.mjs';
const START = '(?<=^|\\n)', END = '(?=\\n|$)', SPACE = `[${PY_SPACE}]*`;
// `_FOOTER` and `_BARE_FOOTER` (inspection reads a bare `Steps: N` when there is no full footer).
const FOOTER = new RegExp(`${START}Steps:${SPACE}\\p{Nd}+${SPACE},[^\\n]*${END}`, 'gu');
const BARE_FOOTER = new RegExp(`${START}Steps:${SPACE}\\p{Nd}+${SPACE}${END}`, 'gu');
const TRAILERS = ['Template:', 'Negative Template:'];
const DELIMITER = '\nNegative prompt:';
/** `_trailer_lines`: empty, or at most one `Template:` line then at most one `Negative Template:` line. */
function trailerLines(text) {
const stripped = pyStrip(text);
const lines = stripped ? stripped.split(/\r?\n|\r/) : [];
const prefixes = lines.map(line => {
const index = TRAILERS.findIndex(name => line.startsWith(name));
return index === -1 ? null : index;
});
return !prefixes.includes(null) && prefixes.every((prefix, index) => index === 0 || prefix > prefixes[index - 1]);
}
/** `_footer(value, inspection=True)`: the unique settings footer match, or null. */
function footer(value) {
let footers = [...value.matchAll(FOOTER)];
if (!footers.length) footers = [...value.matchAll(BARE_FOOTER)];
if (footers.length !== 1) return null;
const [match] = footers;
return trailerLines(value.slice(match.index + match[0].length)) ? match : null;
}
const rstripLineBreaks = text => text.replace(/[\r\n]+$/, '');
/** `_parameters(value, max_text_length, inspection=True)`. */
export function parameters(value, maxTextLength) {
if (typeof value !== 'string' || !value) return result('absent', { reason: 'No A1111 parameters' });
if (pyLen(value) > maxTextLength) return result('unresolved', { reason: 'Parameters exceed the length limit', reasonCode: 'text-limit' });
const match = footer(value);
if (match === null) return result('unresolved', { reason: 'A1111 settings footer is not unique', reasonCode: 'malformed' });
// R4: text that starts with the delimiter has an empty positive (#165).
const body = `\n${rstripLineBreaks(value.slice(0, match.index))}`;
const parts = body.split(DELIMITER);
if (parts.length > 2) return result('ambiguous', { reason: 'Multiple negative prompt delimiters' });
const positive = pyStrip(parts[0]);
if (!positive) return result('unresolved', { reason: EMPTY_POSITIVE_REASON });
const provenance = { format: 'a1111', field: 'parameters', text_source: 'embedded-parameters' };
const side = (text, role) => ({ text, storage_text: text, provenance: { ...provenance, role } });
if (parts.length === 1) return result('absent', { positive: side(positive, 'positive'), reason: 'Negative prompt delimiter is absent' });
const negative = pyStrip(parts[1]);
return result('resolved', { positive: side(positive, 'positive'), negative: side(negative, 'negative') });
}