Say a prompt's role for values kept from pictures
Current node showed "negative prompt" only on values from a workflow. A material's blocks carried no role, and when a picture and a workflow held the same text the merged card kept the picture's empty one, so a negative prompt could be offered to a positive box with nothing to say so. - api/materials.py works the role out by what the prompt was wired to, as the material details already do; it reads the stored workflow, so pictures kept earlier get it too. - node_parameter_sets.js takes the role from whichever source knows it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ec81f8b230
commit
f5a3b5f9ff
@@ -2,6 +2,8 @@
|
||||
|
||||
## v1.63.0 — unreleased
|
||||
|
||||
- 当前节点的提示词卡片现在都标着“正面提示词”或“负面提示词”:从图片存下来的也按它原来接在采样器哪个口上来认,以前只有工作流来源的才标,图片和工作流同时有时标注还会丢。
|
||||
- Current node's prompt cards all say positive or negative now: values kept from a picture are told by what they were wired to in that workflow. Before, only a workflow's said so, and the label was lost when a picture held the same text.
|
||||
- 回到一个存档时,输出节点上的图也跟着换:画布是哪个存档,节点上就是哪个存档跑出来的图。跑了几次就都排在节点里,和批量出图一样,新的在前,最多 16 张。这个存档没有图时节点空着,不留别的存档的图,免得看错。只对这个版本之后跑的图有效。
|
||||
- 回到存档时数字有时不亮、撤销多出一步的问题修好了:画布上有 Preview Image 这类节点时,每隔一次就会这样。
|
||||
- Going back to a save brings its pictures back too: the output nodes show what that save made. Several runs sit on the node together as a batch does, the newest first, up to 16. A save with no picture leaves the node empty rather than showing another save's. This works for runs from this version on.
|
||||
|
||||
@@ -399,6 +399,10 @@ async def api_get_materials_by_node_type(request):
|
||||
continue
|
||||
blocks = [block for block in _material_node_blocks(material, include_values=True) if block["type"] == node_type]
|
||||
if blocks:
|
||||
# A prompt's role, by what it is wired to in the workflow it came from.
|
||||
roles = _prompt_roles_for_workflow(material["workflow"], material.get("promptRoleOverrides"))
|
||||
for block in blocks:
|
||||
block["role"] = (roles.get(str(block["node_id"])) or {}).get("role") or ""
|
||||
# Where the values came from, for Current node: a kept picture, or a node saved there.
|
||||
source = material.get("source") if isinstance(material.get("source"), dict) else {}
|
||||
image = source.get("image") if isinstance(source.get("image"), dict) else {}
|
||||
|
||||
@@ -134,4 +134,16 @@ function sampler() {
|
||||
assert.equal(closestSet(node, entries.slice(0, 1)), null); // too far to speak of
|
||||
}
|
||||
|
||||
{
|
||||
// The same text from a picture and from a workflow: the role of whichever knows it.
|
||||
const text = ['low quality'];
|
||||
globalThis.fetch = async url => ({ ok: true, json: async () => (String(url).includes('/materials/')
|
||||
? { materials: [{ name: 'Picture', timestamp: 5, blocks: [{ title: 'Prompt', role: 'unknown', widgets_values: text }] }] }
|
||||
: String(url).includes('/parameters/')
|
||||
? { groups: [{ recipe_filename: 'r.json', recipe_name: 'Portrait', notebooks: [{ name: 'Set', timestamp: 4, nodes: [{ role: 'negative', widgets_values: text }] }] }] }
|
||||
: { combos: [] }) });
|
||||
const [entry] = await loadParameterSets('CLIPTextEncode');
|
||||
assert.deepEqual([entry.role, entry.sources.length], ['negative', 2]);
|
||||
}
|
||||
|
||||
console.log('node parameter sets: merged sources, kept values, undoable apply, last applied OK');
|
||||
|
||||
@@ -73,6 +73,23 @@ class MaterialsByNodeTypeTests(unittest.IsolatedAsyncioTestCase):
|
||||
response = await materials.api_get_materials_by_node_type(SimpleNamespace(query={'type': 'KSampler'}))
|
||||
self.assertEqual([item['name'] for item in json.loads(response.text)['materials']], ['kept'])
|
||||
|
||||
async def test_a_prompt_says_its_role_by_what_it_is_wired_to(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
workflow = {
|
||||
'nodes': [
|
||||
{'id': 1, 'type': 'CLIPTextEncode', 'widgets_values': ['a quiet lake']},
|
||||
{'id': 2, 'type': 'CLIPTextEncode', 'widgets_values': ['low quality']},
|
||||
{'id': 3, 'type': 'CLIPTextEncode', 'widgets_values': ['not wired']},
|
||||
{'id': 4, 'type': 'KSampler', 'inputs': [{'name': 'positive', 'link': 1}, {'name': 'negative', 'link': 2}], 'widgets_values': [1]},
|
||||
],
|
||||
'links': [[1, 1, 0, 4, 1, 'CONDITIONING'], [2, 2, 0, 4, 2, 'CONDITIONING']],
|
||||
}
|
||||
Path(directory, 'kept.json').write_text(json.dumps({'name': 'kept', 'workflow': workflow, 'kind': 'image_node_selection'}))
|
||||
with mock.patch.object(materials, 'get_materials_dir', lambda: directory):
|
||||
response = await materials.api_get_materials_by_node_type(SimpleNamespace(query={'type': 'CLIPTextEncode'}))
|
||||
blocks = json.loads(response.text)['materials'][0]['blocks']
|
||||
self.assertEqual({block['node_id']: block['role'] for block in blocks}, {1: 'positive', 2: 'negative', 3: 'unknown'})
|
||||
|
||||
async def test_each_says_where_its_values_came_from(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
workflow = {'nodes': [{'id': 1, 'type': 'KSampler', 'widgets_values': [123, 'fixed', 20]}]}
|
||||
|
||||
@@ -26,6 +26,8 @@ async function fetchJson(url, signal) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const KNOWN_ROLES = new Set(['positive', 'negative', 'both']);
|
||||
|
||||
/**
|
||||
* [{ name, role, timestamp, values, sources }], newest first. A source is a material
|
||||
* { kind: 'material', label, filename, timestamp, origin, image, preview } (origin: how it was
|
||||
@@ -51,6 +53,7 @@ export async function loadParameterSets(type, { signal, boxes = [] } = {}) {
|
||||
else {
|
||||
known.sources.push(...entry.sources);
|
||||
known.timestamp = Math.max(known.timestamp, entry.timestamp);
|
||||
if (!KNOWN_ROLES.has(known.role)) known.role = entry.role; // one source may not know it
|
||||
}
|
||||
};
|
||||
for (const material of materials.materials || []) {
|
||||
|
||||
Reference in New Issue
Block a user