From eaa9f7618111a64ad02bf738046b8a6884df6d33 Mon Sep 17 00:00:00 2001 From: vegu-ai-tools <152010387+vegu-ai-tools@users.noreply.github.com> Date: Sun, 16 Nov 2025 13:38:55 +0200 Subject: [PATCH] unified asset library --- src/talemate/agents/visual/analyze.py | 3 +- .../modules/determine-visual-references.json | 612 +++++++++++------- .../agents/visual/websocket_handler.py | 3 +- src/talemate/scene_assets.py | 217 ++++++- src/talemate/server/run.py | 4 + src/talemate/server/scene_assets.py | 4 +- tests/test_graphs.py | 12 +- 7 files changed, 610 insertions(+), 245 deletions(-) diff --git a/src/talemate/agents/visual/analyze.py b/src/talemate/agents/visual/analyze.py index 6ff4254a..e3a054c4 100644 --- a/src/talemate/agents/visual/analyze.py +++ b/src/talemate/agents/visual/analyze.py @@ -56,8 +56,9 @@ class AnalysisMixin: if response.request and response.request.asset_id and response.analysis: scene = active_scene.get() if scene and response.request.asset_id in scene.assets.assets: - asset = scene.assets.assets[response.request.asset_id] + asset = scene.assets.get_asset(response.request.asset_id) asset.meta.analysis = response.analysis + scene.assets.update_asset_meta(response.request.asset_id, asset.meta) scene.saved = False scene.emit_status() diff --git a/src/talemate/agents/visual/modules/determine-visual-references.json b/src/talemate/agents/visual/modules/determine-visual-references.json index db9fc16a..2757c30a 100644 --- a/src/talemate/agents/visual/modules/determine-visual-references.json +++ b/src/talemate/agents/visual/modules/determine-visual-references.json @@ -83,8 +83,8 @@ "title": "List Collector", "id": "78179ea2-5b0e-4a3f-bd16-cf7a0335459b", "properties": {}, - "x": 1365, - "y": 1231, + "x": 1337, + "y": 1666, "width": 140, "height": 81, "collapsed": false, @@ -120,8 +120,8 @@ "response_length": 0, "technical": false }, - "x": 1555, - "y": 971, + "x": 1527, + "y": 1406, "width": 304, "height": 542, "collapsed": false, @@ -157,21 +157,6 @@ "registry": "core/functions/DefineFunction", "base_type": "core/Node" }, - "19e58d48-e6b2-4444-b561-d2d04f2caf3b": { - "title": "FN asset_info", - "id": "19e58d48-e6b2-4444-b561-d2d04f2caf3b", - "properties": { - "name": "asset_info" - }, - "x": 485, - "y": 1301, - "width": 210, - "height": 78, - "collapsed": false, - "inherited": false, - "registry": "core/functions/GetFunction", - "base_type": "core/Node" - }, "047adc9f-f498-42e1-8f5a-2559aa4dcbaf": { "title": "Get Asset", "id": "047adc9f-f498-42e1-8f5a-2559aa4dcbaf", @@ -194,7 +179,7 @@ "x": -941, "y": 1011, "width": 178, - "height": 286, + "height": 306, "collapsed": false, "inherited": false, "registry": "assets/UnpackAssetMeta", @@ -236,8 +221,8 @@ "header": "Assets", "content": null }, - "x": 1095, - "y": 1271, + "x": 1067, + "y": 1706, "width": 228, "height": 102, "collapsed": false, @@ -245,22 +230,6 @@ "registry": "agents/DynamicInstruction", "base_type": "core/Node" }, - "b922d276-1b60-4494-97ec-092916ac0883": { - "title": "Call For Each", - "id": "b922d276-1b60-4494-97ec-092916ac0883", - "properties": { - "copy_items": false, - "argument_name": "item" - }, - "x": 805, - "y": 1351, - "width": 210, - "height": 122, - "collapsed": false, - "inherited": false, - "registry": "core/functions/CallForEach", - "base_type": "core/Node" - }, "a2b4a51d-c353-4961-9189-e53dd7b8a4cc": { "title": "Return", "id": "a2b4a51d-c353-4961-9189-e53dd7b8a4cc", @@ -324,8 +293,8 @@ "properties": { "template": "Scan the `image generation prompt` and determine up to {{ max_references }} to link to the image generation process.\n\nReferences should be used to provide visual guidance only.\n\nHighest priority are character likeness, followed by environment and style.\n\nFor each reference provide a short instruction on what the reference should inform.\n\nAnalyze both the prompt and the references before making your decision.\n\nYour preference should be to select one reference per subject, and your goal should not be to exhaust your reference alloweance at all costs.\n\n### Image generation prompt\n\n``` prompt\n{{ prompt }}\n```" }, - "x": 1105, - "y": 1021, + "x": 1077, + "y": 1456, "width": 210, "height": 153, "collapsed": false, @@ -343,19 +312,6 @@ ], "base_type": "core/DynamicSocketNodeBase" }, - "50a26db5-a93e-4690-ac5c-89fff54dd734": { - "title": "Backend Status", - "id": "50a26db5-a93e-4690-ac5c-89fff54dd734", - "properties": {}, - "x": 875, - "y": 1591, - "width": 170, - "height": 66, - "collapsed": false, - "inherited": false, - "registry": "agents/visual/BackendStatus", - "base_type": "core/Node" - }, "9071af02-b26e-4507-8d15-cd4d4c977bd3": { "title": "Dict Collector", "id": "9071af02-b26e-4507-8d15-cd4d4c977bd3", @@ -413,29 +369,14 @@ "registry": "focal/Argument", "base_type": "core/Node" }, - "7051e54e-1189-4d2f-9818-aa55603aa358": { - "title": "Stage 0", - "id": "7051e54e-1189-4d2f-9818-aa55603aa358", - "properties": { - "stage": 0 - }, - "x": 2905, - "y": 1511, - "width": 210, - "height": 118, - "collapsed": false, - "inherited": false, - "registry": "core/Stage", - "base_type": "core/Node" - }, "e4c27566-d5d5-44d4-8dd5-3dbdb856bcb9": { "title": "Collect AI Function Call Results", "id": "e4c27566-d5d5-44d4-8dd5-3dbdb856bcb9", "properties": { "name": "select_reference" }, - "x": 2250, - "y": 1520, + "x": 2222, + "y": 1955, "width": 269, "height": 78, "collapsed": false, @@ -452,8 +393,8 @@ "retries": 0, "response_length": 1024 }, - "x": 1992, - "y": 1501, + "x": 1964, + "y": 1936, "width": 210, "height": 250, "collapsed": false, @@ -651,73 +592,6 @@ "registry": "core/Input", "base_type": "core/Node" }, - "0f3a0ab4-45bd-4deb-b14e-ab6b673b4d0f": { - "title": "Search Assets", - "id": "0f3a0ab4-45bd-4deb-b14e-ab6b673b4d0f", - "properties": { - "vis_type": "", - "character_name": "", - "tags": [], - "reference_vis_types": [], - "tag_match_mode": "all", - "references_only": true - }, - "x": 445, - "y": 1451, - "width": 329, - "height": 274, - "collapsed": false, - "inherited": false, - "registry": "assets/SearchAssets", - "base_type": "core/Node" - }, - "2cdd9993-bb7b-4e2d-8242-58a96d659494": { - "title": "SET local.selected_references", - "id": "2cdd9993-bb7b-4e2d-8242-58a96d659494", - "properties": { - "name": "selected_references", - "scope": "local" - }, - "x": 2580, - "y": 1500, - "width": 286, - "height": 122, - "collapsed": false, - "inherited": false, - "registry": "state/SetState", - "base_type": "core/Node" - }, - "7abcc346-319c-47a7-93dc-f9392242d5e8": { - "title": "summarizer", - "id": "7abcc346-319c-47a7-93dc-f9392242d5e8", - "properties": { - "agent_name": "summarizer" - }, - "x": 1340, - "y": 970, - "width": 210, - "height": 58, - "collapsed": true, - "inherited": false, - "registry": "agents/GetAgent", - "base_type": "core/Node" - }, - "bf1b63b3-acb3-42a4-bcc7-0959597bc005": { - "title": "GET local.prompt", - "id": "bf1b63b3-acb3-42a4-bcc7-0959597bc005", - "properties": { - "name": "prompt", - "scope": "local" - }, - "x": 790, - "y": 1030, - "width": 210, - "height": 122, - "collapsed": false, - "inherited": false, - "registry": "state/GetState", - "base_type": "core/Node" - }, "db5acc2a-4c09-4792-b503-f064f2ef4ea8": { "title": "IN vis_type", "id": "db5acc2a-4c09-4792-b503-f064f2ef4ea8", @@ -817,41 +691,6 @@ "registry": "state/SetState", "base_type": "core/Node" }, - "3429402d-fc1d-46cf-b159-3aa28673b054": { - "title": "GET local.vis_type", - "id": "3429402d-fc1d-46cf-b159-3aa28673b054", - "properties": { - "name": "vis_type", - "scope": "local" - }, - "x": 40, - "y": 1510, - "width": 210, - "height": 122, - "collapsed": false, - "inherited": false, - "registry": "state/GetState", - "base_type": "core/Node" - }, - "4c9d0de8-cb9c-4b87-9c06-d4031bb76317": { - "title": "List Collector", - "id": "4c9d0de8-cb9c-4b87-9c06-d4031bb76317", - "properties": {}, - "x": 280, - "y": 1520, - "width": 140, - "height": 81, - "collapsed": false, - "inherited": false, - "registry": "data/ListCollector", - "dynamic_inputs": [ - { - "name": "item0", - "type": "*" - } - ], - "base_type": "core/DynamicSocketNodeBase" - }, "4e079b60-a8fc-408e-8dcb-b8f7a08017a0": { "title": "AI Function Callback Metadata", "id": "4e079b60-a8fc-408e-8dcb-b8f7a08017a0", @@ -873,6 +712,153 @@ "registry": "focal/Metadata", "base_type": "core/Node" }, + "19e58d48-e6b2-4444-b561-d2d04f2caf3b": { + "title": "FN asset_info", + "id": "19e58d48-e6b2-4444-b561-d2d04f2caf3b", + "properties": { + "name": "asset_info" + }, + "x": 782, + "y": 1755, + "width": 210, + "height": 78, + "collapsed": true, + "inherited": false, + "registry": "core/functions/GetFunction", + "base_type": "core/Node" + }, + "b922d276-1b60-4494-97ec-092916ac0883": { + "title": "Call For Each", + "id": "b922d276-1b60-4494-97ec-092916ac0883", + "properties": { + "copy_items": false, + "argument_name": "item" + }, + "x": 777, + "y": 1786, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "core/functions/CallForEach", + "base_type": "core/Node" + }, + "1bd33d57-b320-4e0a-a3e6-b88d999e1af6": { + "title": "GET local.vis_type", + "id": "1bd33d57-b320-4e0a-a3e6-b88d999e1af6", + "properties": { + "name": "vis_type", + "scope": "local" + }, + "x": 48, + "y": 1031, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/GetState", + "base_type": "core/Node" + }, + "3d9ef292-f08a-45ee-bb0f-265056c29980": { + "title": "List Collector", + "id": "3d9ef292-f08a-45ee-bb0f-265056c29980", + "properties": {}, + "x": 288, + "y": 1041, + "width": 140, + "height": 81, + "collapsed": false, + "inherited": false, + "registry": "data/ListCollector", + "dynamic_inputs": [ + { + "name": "item0", + "type": "*" + } + ], + "base_type": "core/DynamicSocketNodeBase" + }, + "58d88870-548a-45da-a3b5-f7f254168f9f": { + "title": "SET local.has_references", + "id": "58d88870-548a-45da-a3b5-f7f254168f9f", + "properties": { + "name": "has_references", + "scope": "local" + }, + "x": 1100, + "y": 1107, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/SetState", + "base_type": "core/Node" + }, + "3085f6fc-13be-4788-a318-2b741c7fd82c": { + "title": "SET local.references", + "id": "3085f6fc-13be-4788-a318-2b741c7fd82c", + "properties": { + "name": "references", + "scope": "local" + }, + "x": 1090, + "y": 887, + "width": 210, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/SetState", + "base_type": "core/Node" + }, + "c327d4f3-8334-4cda-afcb-abce468a3f63": { + "title": "Stage 0", + "id": "c327d4f3-8334-4cda-afcb-abce468a3f63", + "properties": { + "stage": 0 + }, + "x": 1410, + "y": 1007, + "width": 210, + "height": 118, + "collapsed": false, + "inherited": false, + "registry": "core/Stage", + "base_type": "core/Node" + }, + "0fd69b8a-4c7b-4ddb-8a91-0826e811c115": { + "title": "Switch", + "id": "0fd69b8a-4c7b-4ddb-8a91-0826e811c115", + "properties": { + "pass_through": false + }, + "x": 342, + "y": 1400, + "width": 210, + "height": 78, + "collapsed": false, + "inherited": false, + "registry": "core/Switch", + "base_type": "core/Node" + }, + "98c75f89-48cb-4645-84cf-e7f6512af35a": { + "title": "List Collector", + "id": "98c75f89-48cb-4645-84cf-e7f6512af35a", + "properties": {}, + "x": 1605, + "y": 2230, + "width": 140, + "height": 81, + "collapsed": false, + "inherited": false, + "registry": "data/ListCollector", + "dynamic_inputs": [ + { + "name": "item0", + "type": "*" + } + ], + "base_type": "core/DynamicSocketNodeBase" + }, "14ebd09d-3fc2-4149-b8df-87f4081be551": { "title": "AI Function Callback", "id": "14ebd09d-3fc2-4149-b8df-87f4081be551", @@ -880,8 +866,8 @@ "name": "my_function", "allow_multiple_calls": true }, - "x": 1315, - "y": 1981, + "x": 1345, + "y": 2220, "width": 210, "height": 102, "collapsed": false, @@ -895,8 +881,8 @@ "properties": { "name": "select_reference" }, - "x": 1030, - "y": 1980, + "x": 1075, + "y": 2220, "width": 210, "height": 78, "collapsed": false, @@ -904,24 +890,150 @@ "registry": "core/functions/GetFunction", "base_type": "core/Node" }, - "98c75f89-48cb-4645-84cf-e7f6512af35a": { - "title": "List Collector", - "id": "98c75f89-48cb-4645-84cf-e7f6512af35a", + "50a26db5-a93e-4690-ac5c-89fff54dd734": { + "title": "Backend Status", + "id": "50a26db5-a93e-4690-ac5c-89fff54dd734", "properties": {}, - "x": 1620, - "y": 1960, - "width": 140, - "height": 81, + "x": 795, + "y": 2020, + "width": 170, + "height": 66, "collapsed": false, "inherited": false, - "registry": "data/ListCollector", - "dynamic_inputs": [ - { - "name": "item0", - "type": "*" - } - ], - "base_type": "core/DynamicSocketNodeBase" + "registry": "agents/visual/BackendStatus", + "base_type": "core/Node" + }, + "50a7431a-2410-4b99-b7eb-13dbf06d2775": { + "title": "GET local.references", + "id": "50a7431a-2410-4b99-b7eb-13dbf06d2775", + "properties": { + "name": "references", + "scope": "local" + }, + "x": 568, + "y": 1850, + "width": 210, + "height": 122, + "collapsed": true, + "inherited": false, + "registry": "state/GetState", + "base_type": "core/Node" + }, + "bf1b63b3-acb3-42a4-bcc7-0959597bc005": { + "title": "GET local.prompt", + "id": "bf1b63b3-acb3-42a4-bcc7-0959597bc005", + "properties": { + "name": "prompt", + "scope": "local" + }, + "x": 878, + "y": 1530, + "width": 210, + "height": 122, + "collapsed": true, + "inherited": false, + "registry": "state/GetState", + "base_type": "core/Node" + }, + "43dcf513-8229-493d-9503-c2fe79a68d18": { + "title": "GET local.has_references", + "id": "43dcf513-8229-493d-9503-c2fe79a68d18", + "properties": { + "name": "has_references", + "scope": "local" + }, + "x": 108, + "y": 1430, + "width": 271, + "height": 124, + "collapsed": true, + "inherited": false, + "registry": "state/GetState", + "base_type": "core/Node" + }, + "7abcc346-319c-47a7-93dc-f9392242d5e8": { + "title": "summarizer", + "id": "7abcc346-319c-47a7-93dc-f9392242d5e8", + "properties": { + "agent_name": "summarizer" + }, + "x": 1380, + "y": 1460, + "width": 210, + "height": 58, + "collapsed": true, + "inherited": false, + "registry": "agents/GetAgent", + "base_type": "core/Node" + }, + "bf3b8b75-ac0f-4e36-a33e-5a7596dfb398": { + "title": "Search Assets", + "id": "bf3b8b75-ac0f-4e36-a33e-5a7596dfb398", + "properties": { + "vis_type": "", + "character_name": "", + "tags": [], + "reference_vis_types": [], + "tag_match_mode": "all", + "references_only": true + }, + "x": 453, + "y": 972, + "width": 329, + "height": 274, + "collapsed": false, + "inherited": false, + "registry": "assets/SearchAssets", + "base_type": "core/Node" + }, + "83ce46ec-45ca-49d0-9bc7-003a855d881c": { + "title": "Compare", + "id": "83ce46ec-45ca-49d0-9bc7-003a855d881c", + "properties": { + "operation": "greater_than", + "tolerance": 0.0001, + "a": 0, + "b": 0 + }, + "x": 840, + "y": 1107, + "width": 210, + "height": 150, + "collapsed": false, + "inherited": false, + "registry": "data/number/Compare", + "base_type": "core/Node" + }, + "2cdd9993-bb7b-4e2d-8242-58a96d659494": { + "title": "SET local.selected_references", + "id": "2cdd9993-bb7b-4e2d-8242-58a96d659494", + "properties": { + "name": "selected_references", + "scope": "local" + }, + "x": 2540, + "y": 1940, + "width": 286, + "height": 122, + "collapsed": false, + "inherited": false, + "registry": "state/SetState", + "base_type": "core/Node" + }, + "7051e54e-1189-4d2f-9818-aa55603aa358": { + "title": "Stage 1", + "id": "7051e54e-1189-4d2f-9818-aa55603aa358", + "properties": { + "stage": 1 + }, + "x": 2870, + "y": 1940, + "width": 210, + "height": 118, + "collapsed": false, + "inherited": false, + "registry": "core/Stage", + "base_type": "core/Node" } }, "edges": { @@ -946,9 +1058,6 @@ "9e43c1f7-dbec-4a10-87d6-bb88f68d02b4.value": [ "598f5544-5ce3-4a3e-a096-84870fafbb7d.nodes" ], - "19e58d48-e6b2-4444-b561-d2d04f2caf3b.fn": [ - "b922d276-1b60-4494-97ec-092916ac0883.fn" - ], "047adc9f-f498-42e1-8f5a-2559aa4dcbaf.asset_id": [ "ac5dc0bc-dfb8-4f5f-aa68-74b2a90a953e.item0" ], @@ -967,9 +1076,6 @@ "48a66502-eed2-4995-9dce-7b68e28a911f.dynamic_instruction": [ "78179ea2-5b0e-4a3f-bd16-cf7a0335459b.item0" ], - "b922d276-1b60-4494-97ec-092916ac0883.results": [ - "48a66502-eed2-4995-9dce-7b68e28a911f.content" - ], "a2b4a51d-c353-4961-9189-e53dd7b8a4cc.value": [ "4e079b60-a8fc-408e-8dcb-b8f7a08017a0.state" ], @@ -982,13 +1088,6 @@ "8b5a72e0-cca6-49ff-bbe2-14f0e78da8d5.result": [ "92d1273b-79d8-4898-b00a-be56ea676246.instructions" ], - "50a26db5-a93e-4690-ac5c-89fff54dd734.can_edit_images": [ - "92d1273b-79d8-4898-b00a-be56ea676246.state" - ], - "50a26db5-a93e-4690-ac5c-89fff54dd734.max_references": [ - "8b5a72e0-cca6-49ff-bbe2-14f0e78da8d5.item1", - "08cc0883-df2e-47c8-84f1-21ebcf4a1d99.max_calls" - ], "9071af02-b26e-4507-8d15-cd4d4c977bd3.dict": [ "a2b4a51d-c353-4961-9189-e53dd7b8a4cc.value" ], @@ -1030,19 +1129,6 @@ "760b9b69-263b-4263-a14f-fc9dee596247.value": [ "a027e49c-9f36-4746-b797-98655b862c65.value" ], - "0f3a0ab4-45bd-4deb-b14e-ab6b673b4d0f.asset_ids": [ - "b922d276-1b60-4494-97ec-092916ac0883.state", - "b922d276-1b60-4494-97ec-092916ac0883.items" - ], - "2cdd9993-bb7b-4e2d-8242-58a96d659494.scope": [ - "7051e54e-1189-4d2f-9818-aa55603aa358.state" - ], - "7abcc346-319c-47a7-93dc-f9392242d5e8.agent": [ - "92d1273b-79d8-4898-b00a-be56ea676246.agent" - ], - "bf1b63b3-acb3-42a4-bcc7-0959597bc005.value": [ - "8b5a72e0-cca6-49ff-bbe2-14f0e78da8d5.item0" - ], "db5acc2a-4c09-4792-b503-f064f2ef4ea8.value": [ "0c306509-fdee-4d2a-ae35-ad0500ed0456.value" ], @@ -1061,15 +1147,34 @@ "0c306509-fdee-4d2a-ae35-ad0500ed0456.value": [ "b23b9f1c-823a-4e3d-a592-45621cbb5f05.state_c" ], - "3429402d-fc1d-46cf-b159-3aa28673b054.value": [ - "4c9d0de8-cb9c-4b87-9c06-d4031bb76317.item0" - ], - "4c9d0de8-cb9c-4b87-9c06-d4031bb76317.list": [ - "0f3a0ab4-45bd-4deb-b14e-ab6b673b4d0f.reference_vis_types" - ], "4e079b60-a8fc-408e-8dcb-b8f7a08017a0.state": [ "5fe01e1b-2eff-4e36-b141-073e4c75297c.nodes" ], + "19e58d48-e6b2-4444-b561-d2d04f2caf3b.fn": [ + "b922d276-1b60-4494-97ec-092916ac0883.fn" + ], + "b922d276-1b60-4494-97ec-092916ac0883.results": [ + "48a66502-eed2-4995-9dce-7b68e28a911f.content" + ], + "1bd33d57-b320-4e0a-a3e6-b88d999e1af6.value": [ + "3d9ef292-f08a-45ee-bb0f-265056c29980.item0" + ], + "3d9ef292-f08a-45ee-bb0f-265056c29980.list": [ + "bf3b8b75-ac0f-4e36-a33e-5a7596dfb398.reference_vis_types" + ], + "58d88870-548a-45da-a3b5-f7f254168f9f.value": [ + "c327d4f3-8334-4cda-afcb-abce468a3f63.state_b" + ], + "3085f6fc-13be-4788-a318-2b741c7fd82c.value": [ + "c327d4f3-8334-4cda-afcb-abce468a3f63.state" + ], + "0fd69b8a-4c7b-4ddb-8a91-0826e811c115.yes": [ + "b922d276-1b60-4494-97ec-092916ac0883.state", + "92d1273b-79d8-4898-b00a-be56ea676246.state" + ], + "98c75f89-48cb-4645-84cf-e7f6512af35a.list": [ + "08cc0883-df2e-47c8-84f1-21ebcf4a1d99.callbacks" + ], "14ebd09d-3fc2-4149-b8df-87f4081be551.callback": [ "98c75f89-48cb-4645-84cf-e7f6512af35a.item0" ], @@ -1079,8 +1184,33 @@ "0d7accb5-ecc5-4282-8a4c-4a9ef2660b6e.name": [ "14ebd09d-3fc2-4149-b8df-87f4081be551.name" ], - "98c75f89-48cb-4645-84cf-e7f6512af35a.list": [ - "08cc0883-df2e-47c8-84f1-21ebcf4a1d99.callbacks" + "50a26db5-a93e-4690-ac5c-89fff54dd734.max_references": [ + "8b5a72e0-cca6-49ff-bbe2-14f0e78da8d5.item1", + "08cc0883-df2e-47c8-84f1-21ebcf4a1d99.max_calls" + ], + "50a7431a-2410-4b99-b7eb-13dbf06d2775.value": [ + "b922d276-1b60-4494-97ec-092916ac0883.items" + ], + "bf1b63b3-acb3-42a4-bcc7-0959597bc005.value": [ + "8b5a72e0-cca6-49ff-bbe2-14f0e78da8d5.item0" + ], + "43dcf513-8229-493d-9503-c2fe79a68d18.value": [ + "0fd69b8a-4c7b-4ddb-8a91-0826e811c115.value" + ], + "7abcc346-319c-47a7-93dc-f9392242d5e8.agent": [ + "92d1273b-79d8-4898-b00a-be56ea676246.agent" + ], + "bf3b8b75-ac0f-4e36-a33e-5a7596dfb398.asset_ids": [ + "3085f6fc-13be-4788-a318-2b741c7fd82c.value" + ], + "bf3b8b75-ac0f-4e36-a33e-5a7596dfb398.asset_count": [ + "83ce46ec-45ca-49d0-9bc7-003a855d881c.a" + ], + "83ce46ec-45ca-49d0-9bc7-003a855d881c.result": [ + "58d88870-548a-45da-a3b5-f7f254168f9f.value" + ], + "2cdd9993-bb7b-4e2d-8242-58a96d659494.scope": [ + "7051e54e-1189-4d2f-9818-aa55603aa358.state" ] }, "groups": [ @@ -1096,10 +1226,10 @@ }, { "title": "Process", - "x": 13, - "y": 815, - "width": 3294, - "height": 1385, + "x": 18, + "y": 1290, + "width": 3094, + "height": 1057, "color": "#3f789e", "font_size": 24, "inherited": false @@ -1143,9 +1273,27 @@ "color": "#b06634", "font_size": 24, "inherited": false + }, + { + "title": "Prepare", + "x": 23, + "y": 812, + "width": 1622, + "height": 470, + "color": "#8AA", + "font_size": 24, + "inherited": false + } + ], + "comments": [ + { + "text": "Determine if there are any reference candidates for this request.", + "x": 60, + "y": 870, + "width": 200, + "inherited": false } ], - "comments": [], "extends": null, "base_type": "core/Graph", "inputs": [], diff --git a/src/talemate/agents/visual/websocket_handler.py b/src/talemate/agents/visual/websocket_handler.py index 23d2b251..c40f59b4 100644 --- a/src/talemate/agents/visual/websocket_handler.py +++ b/src/talemate/agents/visual/websocket_handler.py @@ -98,7 +98,8 @@ class VisualWebsocketHandler(Plugin): sampler_settings=payload.generation_request.sampler_settings, reference_assets=payload.generation_request.reference_assets, ) - scene.assets.assets[asset.id].meta = meta + # Update asset meta and save to library.json + scene.assets.update_asset_meta(asset.id, meta) # Notify frontend and update scene status scene.emit_status() diff --git a/src/talemate/scene_assets.py b/src/talemate/scene_assets.py index 0c8e7073..818e8b8c 100644 --- a/src/talemate/scene_assets.py +++ b/src/talemate/scene_assets.py @@ -5,6 +5,8 @@ import hashlib import os import enum import io +import json +from pathlib import Path from typing import TYPE_CHECKING import pydantic @@ -24,6 +26,7 @@ from talemate.agents.visual.schema import ( SamplerSettings, Resolution, ) +from talemate.path import SCENES_DIR __all__ = [ "Asset", @@ -39,6 +42,7 @@ __all__ = [ "set_character_cover_image_from_image_data", "set_character_cover_image_from_file_path", "set_character_cover_image", + "migrate_scene_assets_to_library", ] log = structlog.get_logger("talemate.scene_assets") @@ -131,7 +135,7 @@ class Asset(pydantic.BaseModel): class SceneAssets: def __init__(self, scene: Scene): self.scene = scene - self.assets = {} + self._assets_cache = None self.cover_image = None @property @@ -154,6 +158,76 @@ class SceneAssets: return asset_path + @property + def _library_path(self) -> str: + """ + Returns the path to the unified library.json file. + """ + return os.path.join(self.asset_directory, "library.json") + + def _load_library(self) -> dict: + """ + Loads the asset library from library.json. + Returns an empty dict if the file doesn't exist. + """ + library_path = self._library_path + if not os.path.exists(library_path): + return {} + + try: + with open(library_path, "r", encoding="utf-8") as f: + data = json.load(f) + return data.get("assets", {}) + except (json.JSONDecodeError, IOError) as e: + log.warning("Failed to load asset library", error=str(e), path=library_path) + return {} + + def _save_library(self, assets_dict: dict): + """ + Saves the asset library to library.json. + """ + library_path = self._library_path + # Ensure directory exists + os.makedirs(os.path.dirname(library_path), exist_ok=True) + + try: + with open(library_path, "w", encoding="utf-8") as f: + json.dump({"assets": assets_dict}, f, indent=2, default=str) + except IOError as e: + log.error("Failed to save asset library", error=str(e), path=library_path) + raise + + @property + def assets(self) -> dict: + """ + Returns the assets dictionary, loading from library.json if needed. + """ + if self._assets_cache is None: + assets_dict = self._load_library() + self._assets_cache = { + asset_id: Asset(**asset_dict) + for asset_id, asset_dict in assets_dict.items() + } + return self._assets_cache + + @assets.setter + def assets(self, value: dict): + """ + Sets the assets dictionary and saves to library.json. + """ + self._assets_cache = value + assets_dict = { + asset_id: asset.model_dump() + for asset_id, asset in value.items() + } + self._save_library(assets_dict) + + def _invalidate_cache(self): + """ + Invalidates the assets cache, forcing a reload from library.json. + """ + self._assets_cache = None + def validate_asset_id(self, asset_id: str) -> bool: """ Validates that the asset id is a valid asset id. @@ -180,11 +254,14 @@ class SceneAssets: def load_assets(self, assets_dict: dict): """ - Loads assets from a dictionary. + Legacy method kept for API compatibility. + Assets are now loaded from library.json automatically via the assets property. + Migration handles moving assets from scene files to library.json. + This method is a no-op since migration runs on server startup. """ - - for asset_id, asset_dict in assets_dict.items(): - self.assets[asset_id] = Asset(**asset_dict) + # No-op: assets are loaded from library.json via the assets property + # Migration handles moving assets from scene files to library.json + pass def transfer_asset(self, source: "SceneAssets", asset_id: str): """ @@ -228,7 +305,10 @@ class SceneAssets: # create the asset object asset = Asset(id=asset_id, file_type=file_extension, media_type=media_type) - self.assets[asset_id] = asset + # Add to assets (this will save to library.json) + current_assets = self.assets + current_assets[asset_id] = asset + self.assets = current_assets return asset @@ -301,6 +381,20 @@ class SceneAssets: return self.assets[asset_id] + def update_asset_meta(self, asset_id: str, meta: AssetMeta): + """ + Updates the metadata for an asset and saves to library.json. + + Args: + asset_id: The ID of the asset to update + meta: The new metadata to set + """ + current_assets = self.assets + if asset_id not in current_assets: + raise KeyError(f"Asset {asset_id} not found") + current_assets[asset_id].meta = meta + self.assets = current_assets # Save to library.json + def get_asset_bytes(self, asset_id: str) -> bytes | None: """ Returns the bytes of the asset with the given id. @@ -385,7 +479,11 @@ class SceneAssets: Removes the asset with the given id. """ - asset = self.assets.pop(asset_id) + current_assets = self.assets + asset = current_assets.pop(asset_id) + + # Save updated library + self.assets = current_assets asset_path = self.asset_directory @@ -563,6 +661,111 @@ async def set_scene_cover_image( return asset_id +def migrate_scene_assets_to_library(root: Path | str | None = None) -> None: + """ + Migrates scene assets from individual scene files to a unified library.json file. + + This function scans all scene JSON files in each project directory and collects + all assets into a single library.json file located at assets/library.json within + each project directory. This migration does not modify the scene files themselves. + + Args: + root: Optional path to the root scenes directory. If None, uses SCENES_DIR. + """ + scenes_root = Path(root) if root else SCENES_DIR + + if not scenes_root.is_dir(): + log.warning("scenes_root_not_found", root=str(scenes_root)) + return + + processed_projects = 0 + processed_scenes = 0 + total_assets = 0 + + try: + # Iterate through all project directories + for project_path in sorted( + (p for p in scenes_root.iterdir() if p.is_dir()), + key=lambda p: p.name + ): + # Find all scene JSON files in this project + scene_files = [ + p for p in project_path.iterdir() + if p.is_file() and p.suffix == ".json" + ] + + if not scene_files: + continue + + # Collect all assets from all scene files + all_assets = {} + + for scene_file in scene_files: + try: + with open(scene_file, "r", encoding="utf-8") as f: + scene_data = json.load(f) + + # Extract assets from scene data + assets_data = scene_data.get("assets", {}).get("assets", {}) + if assets_data: + # Merge assets into the unified collection + # Later scenes will override earlier ones if same asset_id exists + all_assets.update(assets_data) + processed_scenes += 1 + except (json.JSONDecodeError, IOError) as e: + log.warning( + "migrate_scene_assets_failed_to_read", + scene_file=str(scene_file), + error=str(e) + ) + continue + + # If we found any assets, create the library.json file + if all_assets: + assets_dir = project_path / "assets" + assets_dir.mkdir(exist_ok=True) + library_path = assets_dir / "library.json" + + # Only create if it doesn't exist (idempotent migration) + if not library_path.exists(): + try: + with open(library_path, "w", encoding="utf-8") as f: + json.dump({"assets": all_assets}, f, indent=2, default=str) + + processed_projects += 1 + total_assets += len(all_assets) + + log.debug( + "migrated_scene_assets_to_library", + project=str(project_path.name), + assets_count=len(all_assets), + library_path=str(library_path) + ) + except IOError as e: + log.error( + "migrate_scene_assets_failed_to_write", + library_path=str(library_path), + error=str(e) + ) + else: + log.debug( + "library_already_exists", + project=str(project_path.name), + library_path=str(library_path) + ) + + except Exception as e: + log.error("migrate_scene_assets_to_library_failed", error=str(e)) + + if processed_projects > 0: + log.info( + "migration_complete", + projects_processed=processed_projects, + scenes_processed=processed_scenes, + total_assets=total_assets + ) + + async def set_character_cover_image_from_bytes( scene: "Scene", character: "Character", bytes: bytes, override: bool = False ) -> str: diff --git a/src/talemate/server/run.py b/src/talemate/server/run.py index 36d5aa9d..4bc517fb 100644 --- a/src/talemate/server/run.py +++ b/src/talemate/server/run.py @@ -131,6 +131,7 @@ def run_server(args): from talemate.emit.base import emit import talemate.agents.tts.voice_library as voice_library from talemate.changelog import ensure_changelogs_for_all_scenes + from talemate.scene_assets import migrate_scene_assets_to_library # import node libraries import talemate.game.engine.nodes.load_definitions @@ -187,6 +188,9 @@ def run_server(args): # create task to ensure changelogs for all scenes exists loop.create_task(ensure_changelogs_for_all_scenes()) + # migrate scene assets to unified library.json files + migrate_scene_assets_to_library() + # start task to unstall punkt loop.create_task(install_punkt()) diff --git a/src/talemate/server/scene_assets.py b/src/talemate/server/scene_assets.py index 00e48fc1..8a1aa4c2 100644 --- a/src/talemate/server/scene_assets.py +++ b/src/talemate/server/scene_assets.py @@ -97,7 +97,7 @@ class SceneAssetsPlugin(Plugin): ) # Assign meta and emit - self.scene.assets.assets[asset.id].meta = meta + self.scene.assets.update_asset_meta(asset.id, meta) # notify frontend await self.scene.attempt_auto_save() @@ -161,7 +161,7 @@ class SceneAssetsPlugin(Plugin): log.error("reevaluate_format_failed", error=e) # Assign back - self.scene.assets.assets[asset_id].meta = meta + self.scene.assets.update_asset_meta(asset_id, meta) # Notify await self.scene.attempt_auto_save() diff --git a/tests/test_graphs.py b/tests/test_graphs.py index a1fb8150..f9d9566f 100644 --- a/tests/test_graphs.py +++ b/tests/test_graphs.py @@ -141,9 +141,17 @@ def mock_scene_with_assets(): scene.scenes_dir = lambda: test_scenes_dir scene.project_name = "talemate-laboratory" - # Load assets into the scene + # Create library.json file with assets from the scene file if "assets" in test_scene_data and "assets" in test_scene_data["assets"]: - scene.assets.load_assets(test_scene_data["assets"]["assets"]) + assets_dict = test_scene_data["assets"]["assets"] + # Ensure assets directory exists + assets_dir = os.path.join(test_scenes_dir, "talemate-laboratory", "assets") + os.makedirs(assets_dir, exist_ok=True) + + # Create library.json file + library_path = os.path.join(assets_dir, "library.json") + with open(library_path, "w") as f: + json.dump({"assets": assets_dict}, f, indent=2) return scene