diff --git a/CHANGELOG.md b/CHANGELOG.md index 1468bad..d6ff787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/api/materials.py b/api/materials.py index 0d33a07..38c6a12 100644 --- a/api/materials.py +++ b/api/materials.py @@ -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 {} diff --git a/tests/node_parameter_sets.mjs b/tests/node_parameter_sets.mjs index 6ac8de4..0e7b93a 100644 --- a/tests/node_parameter_sets.mjs +++ b/tests/node_parameter_sets.mjs @@ -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'); diff --git a/tests/test_material_browsing.py b/tests/test_material_browsing.py index c86a6d3..588f897 100644 --- a/tests/test_material_browsing.py +++ b/tests/test_material_browsing.py @@ -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]}]} diff --git a/web/modules/node_parameter_sets.js b/web/modules/node_parameter_sets.js index 0413752..cbad2d8 100644 --- a/web/modules/node_parameter_sets.js +++ b/web/modules/node_parameter_sets.js @@ -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 || []) {