diff --git a/Nodes/Uniapi.py b/Nodes/Uniapi.py index 1d95a1d..8d35320 100644 --- a/Nodes/Uniapi.py +++ b/Nodes/Uniapi.py @@ -24,6 +24,7 @@ from ..components.API import api_json_to_requestbody from ..components.API import external_api_backend from ..components.API import api_schema_registry from ..components import file_output +from server import PromptServer class PrimereApiProcessor: CATEGORY = TREE_API @@ -75,12 +76,13 @@ class PrimereApiProcessor: hidden_inputs = { "extra_pnginfo": "EXTRA_PNGINFO", - "prompt_extra": "PROMPT" + "prompt_extra": "PROMPT", + "unique_id": "UNIQUE_ID", } return {"required": cls.required_inputs, "optional": cls.optional_inputs, "hidden": hidden_inputs} - def process_uniapi(self, processor, api_provider, api_service, prompt, negative_prompt = None, batch = 1, reference_images = None, first_image = None, last_image = None, width = 1024, height = 1024, aspect_ratio = '1:1', seed = None, debug_mode = False, **kwargs): + def process_uniapi(self, processor, api_provider, api_service, prompt, negative_prompt = None, batch = 1, reference_images = None, first_image = None, last_image = None, width = 1024, height = 1024, aspect_ratio = '1:1', seed = None, debug_mode = False, unique_id = None, **kwargs): API_SCHEMAS_PATH = os.path.join(PRIMERE_ROOT, 'front_end', 'api_schemas.json') API_CONFIG_PATH = os.path.join(PRIMERE_ROOT, 'json', 'apiconfig.json') API_SCHEMA_REGISTRY = api_schema_registry.load_and_validate_api_schema_registry(API_SCHEMAS_PATH, API_CONFIG_PATH) @@ -188,6 +190,20 @@ class PrimereApiProcessor: elif img_binary_api not in (None, "") and (not isinstance(img_binary_api, list) or len(img_binary_api) > 0): selected_parameters["reference_images"] = img_binary_api + if first_image is not None: + first_image_source = first_image + elif reference_images is not None: + first_image_source = reference_images[0] if isinstance(reference_images, list) else reference_images + else: + first_image_source = None + if first_image_source is not None: + first_image_result = external_api_backend.apply_reference_images_handler( + schema, api_provider, + {"img_binary_api": first_image_source, "loaded_client_for_upload": loaded_client_for_upload} + ) + if isinstance(first_image_result, dict) and first_image_result: + selected_parameters.update(first_image_result) + possible_parameters = schema.get("possible_parameters", {}) if isinstance(schema, dict) else {} if isinstance(possible_parameters, dict): for key in possible_parameters.keys(): @@ -306,6 +322,11 @@ class PrimereApiProcessor: # --- File naming and output path resolution --- auto_save_result = kwargs.get('auto_save_result', False) if auto_save_result and result_image is not None: + if type(result_image).__name__ == "str": + SAVED_IMAGE_PATH = os.path.join(PRIMERE_ROOT, 'front_end', 'images') + SAVED_IMAGE = os.path.join(SAVED_IMAGE_PATH, "file_saved.jpg") + result_image = utility.ImageLoaderFromPath(SAVED_IMAGE) + output_path_input = kwargs.get('output_path', '[time(%Y-%m-%d)]') subpath = kwargs.get('subpath', 'None') add_provider_to_path = kwargs.get('add_provider_to_path', False) @@ -350,17 +371,30 @@ class PrimereApiProcessor: ) Path(folder_paths.temp_directory).mkdir(parents=True, exist_ok=True) - file_output.save_bytes_to_file(save_bytes, output_file, image_extension, image_quality, folder_paths.temp_directory) + try: + saved_path = file_output.save_bytes_to_file(save_bytes, output_file, image_extension, image_quality, folder_paths.temp_directory) - save_data = { - "provider": api_provider, - "service": selected_service or api_service, - "selected_parameters": selected_parameters_output, - "used_values": used_values_output, - # "rendered": rendered_payload, - "raw_payload": raw_payload, - # "api_result": api_result_debug, - } - file_output.save_metadata(save_data, json_file, txt_file, save_data_to_json, save_data_to_txt, used_values_output) + save_data = { + "provider": api_provider, + "service": selected_service or api_service, + "selected_parameters": selected_parameters_output, + "used_values": used_values_output, + # "rendered": rendered_payload, + "raw_payload": raw_payload, + # "api_result": api_result_debug, + } + file_output.save_metadata(save_data, json_file, txt_file, save_data_to_json, save_data_to_txt, used_values_output) + + PromptServer.instance.send_sync("primere.save_result", { + "node_id": unique_id, + "status": "success", + "path": saved_path, + }) + except Exception as save_error: + PromptServer.instance.send_sync("primere.save_result", { + "node_id": unique_id, + "status": "failed", + "error": str(save_error), + }) return (result_image, client, api_provider, schema, rendered_payload, raw_payload, used_values_output, api_schemas, api_result_debug) \ No newline at end of file diff --git a/Workflow/Development/universal_api.json b/Workflow/Development/universal_api.json index 356606a..4288c49 100644 --- a/Workflow/Development/universal_api.json +++ b/Workflow/Development/universal_api.json @@ -1,8 +1,8 @@ { "id": "3ed198dc-bee5-40d4-a269-63c758e8edff", "revision": 0, - "last_node_id": 54, - "last_link_id": 369, + "last_node_id": 74, + "last_link_id": 657, "nodes": [ { "id": 22, @@ -303,13 +303,13 @@ 1262.834388548706 ], "flags": {}, - "order": 20, + "order": 22, "mode": 0, "inputs": [ { "name": "input", "type": "*", - "link": 364 + "link": 652 } ], "outputs": [], @@ -419,13 +419,13 @@ 88 ], "flags": {}, - "order": 19, + "order": 21, "mode": 0, "inputs": [ { "name": "input", "type": "*", - "link": 363 + "link": 651 } ], "outputs": [], @@ -532,13 +532,13 @@ 88 ], "flags": {}, - "order": 18, + "order": 20, "mode": 0, "inputs": [ { "name": "input", "type": "*", - "link": 362 + "link": 650 } ], "outputs": [], @@ -548,7 +548,7 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "" + "" ] }, { @@ -572,7 +572,7 @@ "type": "INT", "links": [ 53, - 360 + 648 ] } ], @@ -584,46 +584,11 @@ "widgets_values": [ "Random", 42, - "Last seed: [611698954884300]" + "Last seed: [275143688740681]" ], "color": "#223", "bgcolor": "#335" }, - { - "id": 9, - "type": "PrimitiveStringMultiline", - "pos": [ - 480.79284780821257, - -541.1178926016723 - ], - "size": [ - 733.2619240917973, - 382.46790301705124 - ], - "flags": {}, - "order": 12, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 355 - ] - } - ], - "properties": { - "Node name for S&R": "PrimitiveStringMultiline", - "cnr_id": "comfy-core", - "ver": "0.14.1" - }, - "widgets_values": [ - "The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images." - ], - "color": "#232", - "bgcolor": "#353" - }, { "id": 42, "type": "PreviewImage", @@ -636,7 +601,7 @@ 581.7454612396214 ], "flags": {}, - "order": 15, + "order": 17, "mode": 0, "inputs": [ { @@ -663,13 +628,13 @@ 739.8065007039345 ], "flags": {}, - "order": 25, + "order": 27, "mode": 0, "inputs": [ { "name": "input", "type": "*", - "link": 369 + "link": 657 } ], "outputs": [], @@ -680,7 +645,7 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "None" + "{\n \"_type\": \"GenerateContentResponse\",\n \"sdk_http_response\": {\n \"_type\": \"HttpResponse\",\n \"headers\": {\n \"content-type\": \"application/json; charset=UTF-8\",\n \"vary\": \"Origin, X-Origin, Referer\",\n \"content-encoding\": \"gzip\",\n \"date\": \"Fri, 06 Mar 2026 21:43:07 GMT\",\n \"server\": \"scaffolding on HTTPServer2\",\n \"x-xss-protection\": \"0\",\n \"x-frame-options\": \"SAMEORIGIN\",\n \"x-content-type-options\": \"nosniff\",\n \"server-timing\": \"gfet4t7; dur=6047\",\n \"alt-svc\": \"h3=\\\":443\\\"; ma=2592000,h3-29=\\\":443\\\"; ma=2592000\",\n \"transfer-encoding\": \"chunked\"\n },\n \"body\": null\n },\n \"candidates\": [\n {\n \"_type\": \"Candidate\",\n \"content\": {\n \"_type\": \"Content\",\n \"parts\": [\n {\n \"_type\": \"Part\",\n \"media_resolution\": null,\n \"code_execution_result\": null,\n \"executable_code\": null,\n \"file_data\": null,\n \"function_call\": null,\n \"function_response\": null,\n \"inline_data\": {\n \"_type\": \"Blob\",\n \"data\": \"[binary data omitted: 1534895 bytes]\",\n \"display_name\": null,\n \"mime_type\": \"image/png\"\n },\n \"text\": null,\n \"thought\": null,\n \"thought_signature\": null,\n \"video_metadata\": null\n }\n ],\n \"role\": \"model\"\n },\n \"citation_metadata\": null,\n \"finish_message\": null,\n \"token_count\": null,\n \"finish_reason\": \"STOP\",\n \"grounding_metadata\": null,\n \"avg_logprobs\": null,\n \"index\": 0,\n \"logprobs_result\": null,\n \"safety_ratings\": null,\n \"url_context_metadata\": null\n }\n ],\n \"create_time\": null,\n \"model_version\": \"gemini-2.5-flash-image\",\n \"prompt_feedback\": null,\n \"response_id\": \"a0qrafmnM7C028oPzozYUQ\",\n \"usage_metadata\": {\n \"_type\": \"GenerateContentResponseUsageMetadata\",\n \"cache_tokens_details\": null,\n \"cached_content_token_count\": null,\n \"candidates_token_count\": 1290,\n \"candidates_tokens_details\": [\n {\n \"_type\": \"ModalityTokenCount\",\n \"modality\": \"IMAGE\",\n \"token_count\": 1290\n }\n ],\n \"prompt_token_count\": 11,\n \"prompt_tokens_details\": [\n {\n \"_type\": \"ModalityTokenCount\",\n \"modality\": \"TEXT\",\n \"token_count\": 11\n }\n ],\n \"thoughts_token_count\": null,\n \"tool_use_prompt_token_count\": null,\n \"tool_use_prompt_tokens_details\": null,\n \"total_token_count\": 1301,\n \"traffic_type\": null\n },\n \"automatic_function_calling_history\": [],\n \"parsed\": null\n}" ], "color": "#432", "bgcolor": "#653" @@ -697,13 +662,13 @@ 1163.5127427084483 ], "flags": {}, - "order": 24, + "order": 26, "mode": 0, "inputs": [ { "name": "input", "type": "*", - "link": 368 + "link": 656 } ], "outputs": [], @@ -714,7 +679,7 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "None" + "[\n {\n \"schema\": {\n \"provider\": \"Gemini\",\n \"service\": \"Nanobanana\",\n \"response_handler\": \"Gemini_Nanobanana.py\",\n \"import_modules\": [\n \"from google import genai\",\n \"from google.genai import types\"\n ],\n \"possible_parameters\": {\n \"aspect_ratio\": [\n \"1:1\",\n \"1:4\",\n \"1:8\",\n \"2:3\",\n \"3:2\",\n \"3:4\",\n \"4:1\",\n \"4:3\",\n \"4:5\",\n \"5:4\",\n \"8:1\",\n \"9:16\",\n \"16:9\",\n \"21:9\"\n ],\n \"resolution\": [\n \"1K\",\n \"512px\",\n \"2K\",\n \"4K\"\n ],\n \"model\": [\n \"gemini-2.5-flash-image\",\n \"gemini-3-pro-image-preview\",\n \"gemini-3.1-flash-image-preview\"\n ],\n \"thinking_level\": [\n \"minimal\",\n \"high\"\n ],\n \"include_thoughts\": [\n false,\n true\n ]\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-2.5-flash-image\"\n },\n \"remove\": [\n \"config.thinking_config\",\n \"thinking_level\",\n \"include_thoughts\"\n ]\n },\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-3-pro-image-preview\"\n },\n \"remove\": [\n \"config.thinking_config\"\n ]\n }\n ],\n \"request\": {\n \"method\": \"SDK\",\n \"endpoint\": \"client.models.generate_content\",\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"{{model}}\",\n \"contents\": [\n \"{{prompt}}\",\n \"{{reference_images}}\"\n ],\n \"config\": {\n \"$call\": \"types.GenerateContentConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"response_modalities\": [\n \"{{config_response_modalities_0}}\"\n ],\n \"image_config\": {\n \"$call\": \"types.ImageConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"aspect_ratio\": \"{{aspect_ratio}}\",\n \"image_size\": \"{{image_size}}\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"selected_parameters\": {\n \"prompt\": \"The cat and the dog playing in the kitchen.\",\n \"width\": 1365,\n \"height\": 768,\n \"first_image\": \"[torch.Tensor omitted: shape=(1, 1012, 1094, 3), dtype=torch.float32]\",\n \"batch\": 1,\n \"seed\": 275143688740681,\n \"debug_mode\": false,\n \"aspect_ratio\": \"16:9\",\n \"resolution\": \"1K\",\n \"model\": \"gemini-2.5-flash-image\",\n \"thinking_level\": \"minimal\",\n \"include_thoughts\": false\n },\n \"used_values\": {\n \"model\": \"gemini-2.5-flash-image\",\n \"prompt\": \"The cat and the dog playing in the kitchen.\",\n \"config_response_modalities_0\": \"IMAGE\",\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n },\n \"selected_service\": \"Nanobanana\",\n \"rendered\": {\n \"provider\": \"Gemini\",\n \"endpoint\": \"client.models.generate_content\",\n \"method\": \"SDK\",\n \"headers\": {},\n \"query\": {},\n \"body\": null,\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"gemini-2.5-flash-image\",\n \"contents\": [\n \"The cat and the dog playing in the kitchen.\"\n ],\n \"config\": {\n \"$call\": \"types.GenerateContentConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"response_modalities\": [\n \"IMAGE\"\n ],\n \"image_config\": {\n \"$call\": \"types.ImageConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n }\n }\n }\n }\n }\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-2.5-flash-image\"\n },\n \"remove\": [\n \"config.thinking_config\",\n \"thinking_level\",\n \"include_thoughts\"\n ]\n },\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-3-pro-image-preview\"\n },\n \"remove\": [\n \"config.thinking_config\"\n ]\n }\n ]\n },\n \"api_result\": {\n \"_type\": \"GenerateContentResponse\",\n \"sdk_http_response\": {\n \"_type\": \"HttpResponse\",\n \"headers\": {\n \"content-type\": \"application/json; charset=UTF-8\",\n \"vary\": \"Origin, X-Origin, Referer\",\n \"content-encoding\": \"gzip\",\n \"date\": \"Fri, 06 Mar 2026 21:43:07 GMT\",\n \"server\": \"scaffolding on HTTPServer2\",\n \"x-xss-protection\": \"0\",\n \"x-frame-options\": \"SAMEORIGIN\",\n \"x-content-type-options\": \"nosniff\",\n \"server-timing\": \"gfet4t7; dur=6047\",\n \"alt-svc\": \"h3=\\\":443\\\"; ma=2592000,h3-29=\\\":443\\\"; ma=2592000\",\n \"transfer-encoding\": \"chunked\"\n },\n \"body\": null\n },\n \"candidates\": [\n {\n \"_type\": \"Candidate\",\n \"content\": {\n \"_type\": \"Content\",\n \"parts\": [\n {\n \"_type\": \"Part\",\n \"media_resolution\": null,\n \"code_execution_result\": null,\n \"executable_code\": null,\n \"file_data\": null,\n \"function_call\": null,\n \"function_response\": null,\n \"inline_data\": {\n \"_type\": \"Blob\",\n \"data\": \"[binary data omitted: 1534895 bytes]\",\n \"display_name\": null,\n \"mime_type\": \"image/png\"\n },\n \"text\": null,\n \"thought\": null,\n \"thought_signature\": null,\n \"video_metadata\": null\n }\n ],\n \"role\": \"model\"\n },\n \"citation_metadata\": null,\n \"finish_message\": null,\n \"token_count\": null,\n \"finish_reason\": \"STOP\",\n \"grounding_metadata\": null,\n \"avg_logprobs\": null,\n \"index\": 0,\n \"logprobs_result\": null,\n \"safety_ratings\": null,\n \"url_context_metadata\": null\n }\n ],\n \"create_time\": null,\n \"model_version\": \"gemini-2.5-flash-image\",\n \"prompt_feedback\": null,\n \"response_id\": \"a0qrafmnM7C028oPzozYUQ\",\n \"usage_metadata\": {\n \"_type\": \"GenerateContentResponseUsageMetadata\",\n \"cache_tokens_details\": null,\n \"cached_content_token_count\": null,\n \"candidates_token_count\": 1290,\n \"candidates_tokens_details\": [\n {\n \"_type\": \"ModalityTokenCount\",\n \"modality\": \"IMAGE\",\n \"token_count\": 1290\n }\n ],\n \"prompt_token_count\": 11,\n \"prompt_tokens_details\": [\n {\n \"_type\": \"ModalityTokenCount\",\n \"modality\": \"TEXT\",\n \"token_count\": 11\n }\n ],\n \"thoughts_token_count\": null,\n \"tool_use_prompt_token_count\": null,\n \"tool_use_prompt_tokens_details\": null,\n \"total_token_count\": 1301,\n \"traffic_type\": null\n },\n \"automatic_function_calling_history\": [],\n \"parsed\": null\n },\n \"api_error\": null\n }\n]" ], "color": "#2a363b", "bgcolor": "#3f5159" @@ -731,13 +696,13 @@ 628.220628990278 ], "flags": {}, - "order": 23, + "order": 25, "mode": 0, "inputs": [ { "name": "input", "type": "*", - "link": 367 + "link": 655 } ], "outputs": [], @@ -748,92 +713,79 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "{\n \"model\": \"gemini-2.5-flash-image\",\n \"prompt\": \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"reference_images\": \"[reference_images omitted]\",\n \"config_response_modalities_0\": \"IMAGE\",\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n}" + "{\n \"model\": \"gemini-2.5-flash-image\",\n \"prompt\": \"The cat and the dog playing in the kitchen.\",\n \"config_response_modalities_0\": \"IMAGE\",\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n}" ], "color": "#232", "bgcolor": "#353" }, { - "id": 15, - "type": "PrimereResolution", + "id": 47, + "type": "PrimereAnyOutput", "pos": [ - 463.6623051612252, - -94.55294258110021 + 341.6491199516921, + 459.7499835308432 ], "size": [ - 361.37166959228534, - 310 + 887.1002652508323, + 1079.130111868482 ], "flags": {}, - "order": 14, + "order": 24, "mode": 0, "inputs": [ { - "name": "seed", - "shape": 7, - "type": "INT", - "link": 53 - }, - { - "name": "model_version", - "shape": 7, - "type": "STRING", - "link": null - }, - { - "name": "model_concept", - "shape": 7, - "type": "STRING", - "link": null - } - ], - "outputs": [ - { - "name": "WIDTH", - "type": "INT", - "links": [ - 357 - ] - }, - { - "name": "HEIGHT", - "type": "INT", - "links": [ - 358 - ] - }, - { - "name": "SQUARE_SHAPE", - "type": "INT", - "links": null - }, - { - "name": "ASPECT_RATIO", - "type": "STRING", - "links": [ - 359 - ] + "name": "input", + "type": "*", + "link": 654 } ], + "outputs": [], + "title": "Primere Any Debug - RAW PAYLOAD", "properties": { - "Node name for S&R": "PrimereResolution", + "Node name for S&R": "PrimereAnyOutput", "cnr_id": "comfyui_primere_nodes", - "ver": "782a54aaf8138894f4bfd2254326af3564da4cbb" + "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "HD screen [16:9]", - false, - 1024, - false, - "Horizontal", - false, - false, - 1.6, - 2.8 + "{\n \"model\": \"gemini-2.5-flash-image\",\n \"contents\": [\n \"The cat and the dog playing in the kitchen.\"\n ],\n \"config\": {\n \"$call\": \"types.GenerateContentConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"response_modalities\": [\n \"IMAGE\"\n ],\n \"image_config\": {\n \"$call\": \"types.ImageConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n }\n }\n }\n }\n}" ], "color": "#232", "bgcolor": "#353" }, + { + "id": 5, + "type": "PrimereAnyOutput", + "pos": [ + -609.6945083896912, + 461.75327312736374 + ], + "size": [ + 926.2206882503098, + 1078.7652401045225 + ], + "flags": {}, + "order": 23, + "mode": 0, + "inputs": [ + { + "name": "input", + "type": "*", + "link": 653 + } + ], + "outputs": [], + "title": "Primere Any Debug - RENDERED", + "properties": { + "Node name for S&R": "PrimereAnyOutput", + "cnr_id": "comfyui_primere_nodes", + "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" + }, + "widgets_values": [ + "{\n \"provider\": \"Gemini\",\n \"endpoint\": \"client.models.generate_content\",\n \"method\": \"SDK\",\n \"headers\": {},\n \"query\": {},\n \"body\": null,\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"gemini-2.5-flash-image\",\n \"contents\": [\n \"The cat and the dog playing in the kitchen.\"\n ],\n \"config\": {\n \"$call\": \"types.GenerateContentConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"response_modalities\": [\n \"IMAGE\"\n ],\n \"image_config\": {\n \"$call\": \"types.ImageConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n }\n }\n }\n }\n }\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-2.5-flash-image\"\n },\n \"remove\": [\n \"config.thinking_config\",\n \"thinking_level\",\n \"include_thoughts\"\n ]\n },\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-3-pro-image-preview\"\n },\n \"remove\": [\n \"config.thinking_config\"\n ]\n }\n ]\n}" + ], + "color": "#322", + "bgcolor": "#533" + }, { "id": 17, "type": "PrimereMultiImage", @@ -846,7 +798,7 @@ 518 ], "flags": {}, - "order": 13, + "order": 15, "mode": 0, "inputs": [ { @@ -926,7 +878,7 @@ "name": "IMAGE_LIST", "type": "IMAGE", "links": [ - 356 + 643 ] }, { @@ -948,7 +900,7 @@ "ver": "782a54aaf8138894f4bfd2254326af3564da4cbb" }, "widgets_values": [ - true, + false, true, 1, true, @@ -964,72 +916,161 @@ "bgcolor": "#653" }, { - "id": 47, - "type": "PrimereAnyOutput", + "id": 15, + "type": "PrimereResolution", "pos": [ - 341.6491199516921, - 459.7499835308432 + 463.6623051612252, + -94.55294258110021 ], "size": [ - 887.1002652508323, - 1079.130111868482 + 361.37166959228534, + 310 ], "flags": {}, - "order": 22, + "order": 16, "mode": 0, "inputs": [ { - "name": "input", - "type": "*", - "link": 366 + "name": "seed", + "shape": 7, + "type": "INT", + "link": 53 + }, + { + "name": "model_version", + "shape": 7, + "type": "STRING", + "link": null + }, + { + "name": "model_concept", + "shape": 7, + "type": "STRING", + "link": null + } + ], + "outputs": [ + { + "name": "WIDTH", + "type": "INT", + "links": [ + 645 + ] + }, + { + "name": "HEIGHT", + "type": "INT", + "links": [ + 646 + ] + }, + { + "name": "SQUARE_SHAPE", + "type": "INT", + "links": null + }, + { + "name": "ASPECT_RATIO", + "type": "STRING", + "links": [ + 647 + ] } ], - "outputs": [], - "title": "Primere Any Debug - RAW PAYLOAD", "properties": { - "Node name for S&R": "PrimereAnyOutput", + "Node name for S&R": "PrimereResolution", "cnr_id": "comfyui_primere_nodes", - "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" + "ver": "782a54aaf8138894f4bfd2254326af3564da4cbb" }, "widgets_values": [ - "{\n \"model\": \"gemini-2.5-flash-image\",\n \"contents\": [\n \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"Debug mode, data or view of [8] reference images ignored.\"\n ],\n \"config\": {\n \"$call\": \"types.GenerateContentConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"response_modalities\": [\n \"IMAGE\"\n ],\n \"image_config\": {\n \"$call\": \"types.ImageConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n }\n }\n }\n }\n}" + "HD screen [16:9]", + false, + 1024, + false, + "Horizontal", + false, + false, + 1.6, + 2.8 ], "color": "#232", "bgcolor": "#353" }, { - "id": 5, - "type": "PrimereAnyOutput", + "id": 9, + "type": "PrimitiveStringMultiline", "pos": [ - -609.6945083896912, - 461.75327312736374 + 480.79284780821257, + -541.1178926016723 ], "size": [ - 926.2206882503098, - 1078.7652401045225 + 733.2619240917973, + 382.46790301705124 ], "flags": {}, - "order": 21, + "order": 12, "mode": 0, - "inputs": [ + "inputs": [], + "outputs": [ { - "name": "input", - "type": "*", - "link": 365 + "name": "STRING", + "type": "STRING", + "links": [ + 642 + ] } ], - "outputs": [], - "title": "Primere Any Debug - RENDERED", "properties": { - "Node name for S&R": "PrimereAnyOutput", - "cnr_id": "comfyui_primere_nodes", - "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" + "Node name for S&R": "PrimitiveStringMultiline", + "cnr_id": "comfy-core", + "ver": "0.14.1" }, "widgets_values": [ - "{\n \"provider\": \"Gemini\",\n \"endpoint\": \"client.models.generate_content\",\n \"method\": \"SDK\",\n \"headers\": {},\n \"query\": {},\n \"body\": null,\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"gemini-2.5-flash-image\",\n \"contents\": [\n \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"Debug mode, data or view of [8] reference images ignored.\"\n ],\n \"config\": {\n \"$call\": \"types.GenerateContentConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"response_modalities\": [\n \"IMAGE\"\n ],\n \"image_config\": {\n \"$call\": \"types.ImageConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n }\n }\n }\n }\n }\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-2.5-flash-image\"\n },\n \"remove\": [\n \"config.thinking_config\",\n \"thinking_level\",\n \"include_thoughts\"\n ]\n },\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-3-pro-image-preview\"\n },\n \"remove\": [\n \"config.thinking_config\"\n ]\n }\n ]\n}" + "The cat and the dog playing in the kitchen." ], - "color": "#322", - "bgcolor": "#533" + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 58, + "type": "LoadImage", + "pos": [ + -600.5867539794923, + 16.56309913330077 + ], + "size": [ + 327.5351346026613, + 386.2967736602784 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 644 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "Node name for S&R": "LoadImage", + "cnr_id": "comfy-core", + "ver": "0.14.1" + }, + "widgets_values": [ + "2020-12-17_10-46-18.jpg", + "image" + ], + "color": "#432", + "bgcolor": "#653" }, { "id": 27, @@ -1043,14 +1084,14 @@ 1628.4858571984864 ], "flags": {}, - "order": 17, + "order": 19, "mode": 0, "inputs": [ { "name": "images", "shape": 7, "type": "IMAGE", - "link": 361 + "link": 649 }, { "name": "image_metadata", @@ -1078,7 +1119,40 @@ ] }, { - "id": 54, + "id": 55, + "type": "PrimitiveStringMultiline", + "pos": [ + 1408.3168031778405, + -1012.4710264421508 + ], + "size": [ + 733.2619240917973, + 382.46790301705124 + ], + "flags": {}, + "order": 14, + "mode": 4, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [] + } + ], + "properties": { + "Node name for S&R": "PrimitiveStringMultiline", + "cnr_id": "comfy-core", + "ver": "0.14.1" + }, + "widgets_values": [ + "The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\n\ne:\\!Works\\AI\\AIPICS\\ComfyAPI\\[time(%Y-%m-Week-%W)]" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 74, "type": "PrimereApiProcessor", "pos": [ -261.240011586027, @@ -1089,13 +1163,13 @@ 950 ], "flags": {}, - "order": 16, + "order": 18, "mode": 0, "inputs": [ { "name": "prompt", "type": "STRING", - "link": 355 + "link": 642 }, { "name": "negative_prompt", @@ -1107,13 +1181,13 @@ "name": "reference_images", "shape": 7, "type": "IMAGE", - "link": 356 + "link": 643 }, { "name": "first_image", "shape": 7, "type": "IMAGE", - "link": null + "link": 644 }, { "name": "last_image", @@ -1125,25 +1199,25 @@ "name": "width", "shape": 7, "type": "INT", - "link": 357 + "link": 645 }, { "name": "height", "shape": 7, "type": "INT", - "link": 358 + "link": 646 }, { "name": "aspect_ratio", "shape": 7, "type": "STRING", - "link": 359 + "link": 647 }, { "name": "seed", "shape": 7, "type": "INT", - "link": 360 + "link": 648 } ], "outputs": [ @@ -1151,63 +1225,63 @@ "name": "RESULT", "type": "IMAGE", "links": [ - 361 + 649 ] }, { "name": "CLIENT", "type": "APICLIENT", "links": [ - 362 + 650 ] }, { "name": "PROVIDER", "type": "STRING", "links": [ - 363 + 651 ] }, { "name": "SCHEMA", "type": "TUPLE", "links": [ - 364 + 652 ] }, { "name": "RENDERED", "type": "TUPLE", "links": [ - 365 + 653 ] }, { "name": "RAW_PAYLOAD", "type": "TUPLE", "links": [ - 366 + 654 ] }, { "name": "REQUEST_BODY", "type": "TUPLE", "links": [ - 367 + 655 ] }, { "name": "API_SCHEMAS", "type": "TUPLE", "links": [ - 368 + 656 ] }, { "name": "API_RESULT", "type": "TUPLE", "links": [ - 369 + 657 ] } ], @@ -1221,7 +1295,7 @@ "Gemini", "Nanobanana", 1, - false, + true, "e:\\!Works\\AI\\AIPICS\\ComfyAPI\\[time(%Y-%m-Week-%W)]", "None", true, @@ -1231,7 +1305,7 @@ "_", true, true, - 1, + 2, false, "jpg", 95, @@ -1352,120 +1426,128 @@ "IMAGE" ], [ - 355, + 642, 9, 0, - 54, + 74, 0, "STRING" ], [ - 356, + 643, 17, 0, - 54, + 74, 2, "IMAGE" ], [ - 357, + 644, + 58, + 0, + 74, + 3, + "IMAGE" + ], + [ + 645, 15, 0, - 54, + 74, 5, "INT" ], [ - 358, + 646, 15, 1, - 54, + 74, 6, "INT" ], [ - 359, + 647, 15, 3, - 54, + 74, 7, "STRING" ], [ - 360, + 648, 16, 0, - 54, + 74, 8, "INT" ], [ - 361, - 54, + 649, + 74, 0, 27, 0, "IMAGE" ], [ - 362, - 54, + 650, + 74, 1, 2, 0, "APICLIENT" ], [ - 363, - 54, + 651, + 74, 2, 3, 0, "STRING" ], [ - 364, - 54, + 652, + 74, 3, 4, 0, "TUPLE" ], [ - 365, - 54, + 653, + 74, 4, 5, 0, "TUPLE" ], [ - 366, - 54, + 654, + 74, 5, 47, 0, "TUPLE" ], [ - 367, - 54, + 655, + 74, 6, 6, 0, "TUPLE" ], [ - 368, - 54, + 656, + 74, 7, 8, 0, "TUPLE" ], [ - 369, - 54, + 657, + 74, 8, 44, 0, @@ -1476,10 +1558,10 @@ "config": {}, "extra": { "ds": { - "scale": 0.6830134553650712, + "scale": 0.9090909090909098, "offset": [ - 1375.4683395028358, - 755.2390354026397 + 1074.1559852024538, + 856.1419736308026 ] }, "frontendVersion": "1.39.19" diff --git a/Workflow/Manual/nodes/uniapi.md b/Workflow/Manual/nodes/uniapi.md index 3ee0ee6..38faf7e 100644 --- a/Workflow/Manual/nodes/uniapi.md +++ b/Workflow/Manual/nodes/uniapi.md @@ -811,7 +811,7 @@ File saving only runs when `auto_save_result` is ON and the API returned a valid | Input | Default | Description | |---|---|---| -| `output_path` | `[time(%Y-%m-%d)]` | Base output directory. Supports `[time(...)]` tokens. Relative paths are anchored to the ComfyUI output directory. Absolute paths are used as-is. | +| `output_path` | `[time(%Y-%m-%d)]` | Base output directory. Supports `[time(...)]` tokens. Relative paths are anchored to the ComfyUI output directory. Absolute paths are used as-is — e.g. `e:\!Works\AI\AIPICS\ComfyAPI\[time(%Y-%m-Week-%W)]` saves outside ComfyUI to any location on disk. | | `subpath` | `Project` | Fixed subdirectory appended after provider/service/model dirs. Select `None` to skip. | | `add_provider_to_path` | OFF | Adds the API provider name as a subdirectory (e.g. `Gemini`). | | `add_service_to_path` | OFF | Adds the selected service name as a subdirectory (e.g. `Imagen`). | @@ -855,6 +855,9 @@ The actual file type is detected from the API response bytes (MIME detection), n | `text/*` | `.txt` (UTF-8) | | other / unknown | extension from `image_extension` | +> **Important — non-image results have no display or player.** +> Video, audio, and text results are saved to disk only. There is no preview, no image output, and no playback inside ComfyUI. When the file is saved successfully, a toast notification appears on the node with the saved file path. If `auto_save_result` is **OFF**, the result is permanently lost — it exists only in memory for the duration of that execution and is not recoverable afterward. + ### Metadata files | Input | Default | Description | diff --git a/components/API/references/Gemini_VEO.py b/components/API/references/Gemini_VEO.py new file mode 100644 index 0000000..4b8e887 --- /dev/null +++ b/components/API/references/Gemini_VEO.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import io +from typing import Any +import numpy as np +from PIL import Image +from google.genai import types + + +def handle_reference_images(img_binary_api: Any = None, temp_file_ref: str = "", loaded_client_for_upload: Any = None, **_: Any): + if img_binary_api is None: + return {} + if type(img_binary_api).__name__ == "Tensor": + img_array = (img_binary_api[0].cpu().numpy() * 255).astype(np.uint8) + pil_image = Image.fromarray(img_array) + buffer = io.BytesIO() + pil_image.save(buffer, format="PNG") + image_bytes = buffer.getvalue() + return {"first_image": types.Image(image_bytes=image_bytes, mime_type="image/png")} + return {} diff --git a/components/API/responses/Gemini_VEO.py b/components/API/responses/Gemini_VEO.py new file mode 100644 index 0000000..c664f5f --- /dev/null +++ b/components/API/responses/Gemini_VEO.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import time +import os +import random +import folder_paths + + +def handle_response(api_result, schema=None, loaded_client=None, response_url=None, client=None, sdk_context=None): + if api_result is None: + return [None, None] + + operation = api_result + while not operation.done: + time.sleep(10) + operation = client.operations.get(operation) + + generated_videos = getattr(getattr(operation, 'response', None), 'generated_videos', None) + if not generated_videos: + return [None, None] + + generated_video = generated_videos[0] + tmp_path = os.path.join(folder_paths.temp_directory, f"veo_{random.randint(10000, 99999)}.mp4") + client.files.download(file=generated_video.video) + generated_video.video.save(tmp_path) + + with open(tmp_path, 'rb') as f: + video_bytes = f.read() + + return ["video_result", video_bytes] diff --git a/front_end/api_schemas.json b/front_end/api_schemas.json index 5885542..52bcc29 100644 --- a/front_end/api_schemas.json +++ b/front_end/api_schemas.json @@ -68,16 +68,17 @@ "VEO": { "provider": "Gemini", "service": "VEO", + "reference_images_handler": "Gemini_VEO.py", "import_modules": [ "from google import genai", "from google.genai import types" ], "possible_parameters": { - "aspect_ratio": ["9:16", "16:9"], - "durationSeconds": ["4", "6", "8"], - "personGeneration": ["allow_all", "allow_adult", "dont_allow"], - "resolution": ["720p", "1080p", "4K"], - "model": ["veo-3.0-generate", "veo-3.0-fast-generate", "veo-3.1-generate-preview", "veo-3.1-generate", "veo-3.1-fast-generate-preview", "veo-3.1-fast-generate"] + "model": ["veo-2.0-generate-001", "veo-3.1-fast-generate-preview", "veo-3.1-generate-preview"], + "aspect_ratio": ["16:9", "16:10", "9:16", "10:16"], + "duration_seconds": [4, 5, 6, 7, 8], + "person_generation": ["allow_adult", "allow_all", "dont_allow"], + "resolution": ["720p", "1080p", "4K"] }, "request": { "method": "SDK", @@ -93,11 +94,13 @@ "$args": [], "$kwargs": { "aspectRatio": "{{aspect_ratio}}", + "number_of_videos": "{{batch}}", "resolution": "{{config_resolution}}", - "durationSeconds": "{{durationSeconds}}", - "personGeneration": "{{personGeneration}}", + "durationSeconds": "{{duration_seconds}}", + "personGeneration": "{{person_generation}}", "reference_images": "{{reference_images}}", - "last_frame": "{{last_image}}" + "last_frame": "{{last_image}}", + "negative_prompt": "{{negative_prompt}}" } } } diff --git a/front_end/images/file_saved.jpg b/front_end/images/file_saved.jpg new file mode 100644 index 0000000..724f090 Binary files /dev/null and b/front_end/images/file_saved.jpg differ diff --git a/front_end/primere_output.js b/front_end/primere_output.js index 759d614..a7bef66 100644 --- a/front_end/primere_output.js +++ b/front_end/primere_output.js @@ -389,46 +389,52 @@ class PreviewSaver { saveImageName = state.targetFileName; } + const renderToCanvas = (blob) => new Promise((resolve, reject) => { + const blobUrl = URL.createObjectURL(blob); + const img = new Image(); + img.onload = () => { + URL.revokeObjectURL(blobUrl); + const canvas = document.createElement('canvas'); + let w = img.naturalWidth; + let h = img.naturalHeight; + if (maxWidth && w > maxWidth) { h = Math.round(h * maxWidth / w); w = maxWidth; } + if (maxHeight && h > maxHeight) { w = Math.round(w * maxHeight / h); h = maxHeight; } + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + ctx.drawImage(img, 0, 0, w, h); + resolve(canvas); + }; + img.onerror = () => { + URL.revokeObjectURL(blobUrl); + reject(new Error('Failed to decode image: ' + imageName)); + }; + img.src = blobUrl; + }); + fetch(imageSource) .then((res) => res.blob()) - .then((blob) => { - const reader = new FileReader(); - reader.onloadend = () => { - const file = dataURLtoFile(reader.result, imageName); - loadImage(file, function (img) { - if (typeof img.toDataURL === "function") { - const resampledImage = img.toDataURL(imgMime, wv.targetQuality); - if (wv.saveMode === false) { - downloadImage(resampledImage, extension, saveImageName); - } else { - sendPOSTmessage(JSON.stringify({ - "PreviewTarget": wv.previewTarget, - "PreviewTargetOriginal": state.selectedTarget, - "extension": extension, - "ImageName": imageName, - "ImagePath": ImagePath, - "SaveImageName": saveImageName, - "maxWidth": img.width, - "maxHeight": img.height, - "TargetQuality": wv.targetQuality, - "PrwSaveMode": wv.prwSaveMode, - })); - } - } else { - alert('Source image: ' + imageName + ' does not exist, maybe deleted.'); - } - }, { - maxWidth: maxWidth, - maxHeight: maxHeight, - canvas: true, - pixelRatio: 1, - downsamplingRatio: wv.targetQuality / 100, - orientation: true, - imageSmoothingEnabled: true, - imageSmoothingQuality: 'high', - }); - }; - reader.readAsDataURL(blob); + .then((blob) => renderToCanvas(blob)) + .then((canvas) => { + const resampledImage = canvas.toDataURL(imgMime, wv.targetQuality / 100); + if (wv.saveMode === false) { + downloadImage(resampledImage, extension, saveImageName); + } else { + sendPOSTmessage(JSON.stringify({ + "PreviewTarget": wv.previewTarget, + "PreviewTargetOriginal": state.selectedTarget, + "extension": extension, + "ImageName": imageName, + "ImagePath": ImagePath, + "SaveImageName": saveImageName, + "maxWidth": canvas.width, + "maxHeight": canvas.height, + "TargetQuality": wv.targetQuality, + "PrwSaveMode": wv.prwSaveMode, + })); + } }) .catch((err) => { alert('Failed to load source image: ' + err.message); diff --git a/front_end/primere_uniapi.js b/front_end/primere_uniapi.js index 2f932b7..848c0a6 100644 --- a/front_end/primere_uniapi.js +++ b/front_end/primere_uniapi.js @@ -6,6 +6,106 @@ const SCHEMA_URL = new URL("/extensions/ComfyUI_Primere_Nodes/api_schemas.json", let schemaCache = null; let schemaPromise = null; +const TOAST_DURATION_MS = 10000; // toast auto-dismiss delay in milliseconds + +function showToast(status, message) { + let container = document.getElementById("primere-toast-container"); + if (!container) { + container = document.createElement("div"); + container.id = "primere-toast-container"; + Object.assign(container.style, { + position: "fixed", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + zIndex: "99999", + display: "flex", + flexDirection: "column", + gap: "8px", + pointerEvents: "none", + }); + document.body.appendChild(container); + } + + const isSuccess = status === "success"; + const accentColor = isSuccess ? "#4caf50" : "#f44336"; + const icon = isSuccess ? "✔" : "✖"; + + const toast = document.createElement("div"); + Object.assign(toast.style, { + position: "relative", + overflow: "hidden", + background: "#1e1e1e", + border: `1px solid ${accentColor}`, + borderLeft: `4px solid ${accentColor}`, + borderRadius: "6px", + padding: "10px 14px 10px 12px", + minWidth: "280px", + maxWidth: "440px", + display: "flex", + alignItems: "flex-start", + gap: "10px", + pointerEvents: "all", + boxShadow: "0 4px 16px rgba(0,0,0,0.6)", + opacity: "0", + transform: "translateY(-12px)", + transition: "opacity 0.25s ease, transform 0.25s ease", + fontFamily: "sans-serif", + fontSize: "15px", + color: "#e0e0e0", + cursor: "default", + }); + + const iconEl = document.createElement("span"); + iconEl.textContent = icon; + Object.assign(iconEl.style, { color: accentColor, fontSize: "15px", flexShrink: "0", lineHeight: "1.5" }); + + const textEl = document.createElement("span"); + textEl.textContent = message; + Object.assign(textEl.style, { flex: "1", lineHeight: "1.5", wordBreak: "break-all" }); + + const closeBtn = document.createElement("button"); + closeBtn.textContent = "✕"; + Object.assign(closeBtn.style, { + background: "none", border: "none", color: "#888", cursor: "pointer", + fontSize: "13px", padding: "0 0 0 6px", flexShrink: "0", lineHeight: "1.5", + }); + closeBtn.addEventListener("mouseover", () => { closeBtn.style.color = "#ccc"; }); + closeBtn.addEventListener("mouseout", () => { closeBtn.style.color = "#888"; }); + + const progress = document.createElement("div"); + Object.assign(progress.style, { + position: "absolute", bottom: "0", left: "0", + height: "3px", width: "100%", + background: accentColor, + transformOrigin: "left", + transform: "scaleX(1)", + transition: `transform ${TOAST_DURATION_MS / 1000}s linear`, + }); + + toast.appendChild(iconEl); + toast.appendChild(textEl); + toast.appendChild(closeBtn); + toast.appendChild(progress); + container.appendChild(toast); + + requestAnimationFrame(() => requestAnimationFrame(() => { + toast.style.opacity = "1"; + toast.style.transform = "translateY(0)"; + progress.style.transform = "scaleX(0)"; + })); + + function dismiss() { + clearTimeout(timer); + toast.style.opacity = "0"; + toast.style.transform = "translateY(-12px)"; + setTimeout(() => toast.remove(), 280); + } + + const timer = setTimeout(dismiss, TOAST_DURATION_MS); + closeBtn.addEventListener("click", dismiss); +} + async function loadSchemas() { if (schemaCache) { return schemaCache; @@ -350,6 +450,22 @@ async function initializeUniApiNode(node) { ctx.restore(); }; + const onSaveResult = (event) => { + const detail = event.detail; + if (String(detail?.node_id) !== String(node.id)) return; + if (detail.status === "success") { + showToast("success", `Saved: ${detail.path}`); + } else { + showToast("error", `Save failed: ${detail.error}`); + } + }; + app.api.addEventListener("primere.save_result", onSaveResult); + + const originalOnRemoved = node.onRemoved; + node.onRemoved = function () { + originalOnRemoved?.call(this); + app.api.removeEventListener("primere.save_result", onSaveResult); + }; const originalOnWidgetChanged = node.onWidgetChanged; node.onWidgetChanged = function (name, value, oldValue, widget) {