diff --git a/Nodes/Uniapi.py b/Nodes/Uniapi.py index f84f90e..85b3c4f 100644 --- a/Nodes/Uniapi.py +++ b/Nodes/Uniapi.py @@ -11,7 +11,6 @@ import random import argparse import json import copy -import importlib from pathlib import Path from typing import Any import requests @@ -60,12 +59,13 @@ class PrimereApiProcessor: "prompt_extra": "PROMPT" } - # for key, values in external_api_backend.parameter_options(cls).items(): - # required_inputs[key] = (values,) - 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): + 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) + img_binary_api = [] WORKFLOWDATA = kwargs['extra_pnginfo']['workflow']['nodes'] @@ -104,7 +104,7 @@ class PrimereApiProcessor: config_json = self.API_RESULT client, api_provider = api_helper.create_api_client(api_provider, config_json) - schema, selected_service = api_schema_registry.get_schema(self.API_SCHEMA_REGISTRY, api_provider, api_service) + schema, selected_service = api_schema_registry.get_schema(API_SCHEMA_REGISTRY, api_provider, api_service) if schema is None: schema = { "provider": api_provider, @@ -119,6 +119,10 @@ class PrimereApiProcessor: schema["provider"] = api_provider schema["service"] = selected_service or api_service + schema_import_modules = schema.get("import_modules", []) if isinstance(schema, dict) else [] + if not isinstance(schema_import_modules, list): + raise RuntimeError("Schema key 'import_modules' must be a list of import statements.") + schema["import_modules"] = schema_import_modules selected_parameters = {"prompt": prompt} selected_parameters = {"width": width} @@ -187,12 +191,9 @@ class PrimereApiProcessor: # context, allowed_roots = external_api_backend.build_sdk_context(rendered, client) - try: - from google.genai import types as genai_types - context["types"] = genai_types - allowed_roots.add("types") - except Exception: - pass + imported_context, imported_roots = external_api_backend.load_import_modules(schema_import_modules) + context.update(imported_context) + allowed_roots.update(imported_roots) if debug_mode: return (client, api_provider, schema, rendered_payload, None, api_result, None) diff --git a/Workflow/Development/universal_api.json b/Workflow/Development/universal_api.json index 5db9e52..c60688a 100644 --- a/Workflow/Development/universal_api.json +++ b/Workflow/Development/universal_api.json @@ -37,7 +37,7 @@ "widgets_values": [ "Random", 42, - "Last seed: [772579893417631]" + "Last seed: [499771936453052]" ], "color": "#223", "bgcolor": "#335" @@ -101,7 +101,7 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "" + "" ] }, { @@ -132,7 +132,7 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "({'schema': {'provider': 'Gemini', 'service': 'Nanobanana_V1', 'possible_parameters': {'aspect_ratio': ['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'], 'resolution': ['1K', '2K', '4K'], 'model': ['gemini-3-pro-image-preview', 'gemini-2.5-flash-image']}, 'request': {'method': 'SDK', 'endpoint': 'client.models.generate_content', 'sdk_call': {'args': [], 'kwargs': {'model': '{{model}}', 'contents': ['{{prompt}}', '{{reference_images}}'], 'config': {'$call': 'types.GenerateContentConfig', '$args': [], '$kwargs': {'response_modalities': ['{{config_response_modalities_0}}'], 'image_config': {'$call': 'types.ImageConfig', '$args': [], '$kwargs': {'aspect_ratio': '{{aspect_ratio}}', 'image_size': '{{image_size}}'}}}}}}}}, 'selected_parameters': {'height': 810, 'debug_mode': False, 'batch': 1, 'aspect_ratio': '3:2', 'seed': 772579893417631, 'width': 1295, '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.\", 'resolution': '1K', 'model': 'gemini-2.5-flash-image'}, 'used_values': {'aspect_ratio': '3:2', 'config_response_modalities_0': 'IMAGE', 'image_size': '1K', 'model': 'gemini-2.5-flash-image', '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.\", 'reference_images': 'default_reference_images'}, 'selected_service': 'Nanobanana_V1', 'rendered': {'provider': 'Gemini', 'endpoint': 'client.models.generate_content', 'method': 'SDK', 'headers': {}, 'query': {}, 'body': None, 'sdk_call': {'args': [], 'kwargs': {'model': 'gemini-2.5-flash-image', 'contents': [\"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.\", 'default_reference_images'], 'config': {'$call': 'types.GenerateContentConfig', '$args': [], '$kwargs': {'response_modalities': ['IMAGE'], 'image_config': {'$call': 'types.ImageConfig', '$args': [], '$kwargs': {'aspect_ratio': '3:2', 'image_size': '1K'}}}}}}}, 'api_result': GenerateContentResponse(\n automatic_function_calling_history=[],\n candidates=[\n Candidate(\n content=Content(\n parts=[\n Part(\n inline_data=Blob(\n data=<... Max depth ...>,\n mime_type=<... Max depth ...>\n )\n ),\n ],\n role='model'\n ),\n finish_reason=,\n index=0\n ),\n ],\n model_version='gemini-2.5-flash-image',\n response_id='ND-jaeqwOMyynsEP7Oqx8A4',\n sdk_http_response=HttpResponse(\n headers=\n ),\n usage_metadata=GenerateContentResponseUsageMetadata(\n candidates_token_count=1290,\n candidates_tokens_details=[\n ModalityTokenCount(\n modality=,\n token_count=1290\n ),\n ],\n prompt_token_count=145,\n prompt_tokens_details=[\n ModalityTokenCount(\n modality=,\n token_count=145\n ),\n ],\n total_token_count=1435\n )\n), 'api_error': None},)" + "None" ] }, { @@ -163,7 +163,7 @@ "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 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 \"default_reference_images\"\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\": \"3:2\",\n \"image_size\": \"1K\"\n }\n }\n }\n }\n }\n }\n}" + "{\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 \"default_reference_images\"\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}" ], "color": "#322", "bgcolor": "#533" @@ -196,7 +196,7 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "{\n \"provider\": \"Gemini\",\n \"service\": \"Nanobanana_V1\",\n \"possible_parameters\": {\n \"aspect_ratio\": [\n \"1:1\",\n \"2:3\",\n \"3:2\",\n \"3:4\",\n \"4:3\",\n \"4:5\",\n \"5:4\",\n \"9:16\",\n \"16:9\",\n \"21:9\"\n ],\n \"resolution\": [\n \"1K\",\n \"2K\",\n \"4K\"\n ],\n \"model\": [\n \"gemini-3-pro-image-preview\",\n \"gemini-2.5-flash-image\"\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 \"provider\": \"Gemini\",\n \"service\": \"Nanobanana_V1\",\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 \"2:3\",\n \"3:2\",\n \"3:4\",\n \"4:3\",\n \"4:5\",\n \"5:4\",\n \"9:16\",\n \"16:9\",\n \"21:9\"\n ],\n \"resolution\": [\n \"1K\",\n \"2K\",\n \"4K\"\n ],\n \"model\": [\n \"gemini-3-pro-image-preview\",\n \"gemini-2.5-flash-image\"\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}" ], "color": "#322", "bgcolor": "#533" @@ -229,7 +229,7 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "sdk_http_response=HttpResponse(\n headers=\n) candidates=[Candidate(\n content=Content(\n parts=[\n Part(\n inline_data=Blob(\n data=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x04\\xe0\\x00\\x00\\x03@\\x08\\x02\\x00\\x00\\x00\\rb\\x85G\\x00\\x00\\x17\\x7fcaBX\\x00\\x00\\x17\\x7fjumb\\x00\\x00\\x00\\x1ejumdc2pa\\x00\\x11\\x00\\x10\\x80\\x00\\x00\\xaa\\x008\\x9bq\\x03c2pa\\x00\\x00\\x00\\x17Yjumb\\x00\\x00\\x00Gjumdc2...',\n mime_type='image/png'\n )\n ),\n ],\n role='model'\n ),\n finish_reason=,\n index=0\n)] create_time=None model_version='gemini-2.5-flash-image' prompt_feedback=None response_id='ND-jaeqwOMyynsEP7Oqx8A4' usage_metadata=GenerateContentResponseUsageMetadata(\n candidates_token_count=1290,\n candidates_tokens_details=[\n ModalityTokenCount(\n modality=,\n token_count=1290\n ),\n ],\n prompt_token_count=145,\n prompt_tokens_details=[\n ModalityTokenCount(\n modality=,\n token_count=145\n ),\n ],\n total_token_count=1435\n) automatic_function_calling_history=[] parsed=None" + "None" ] }, { @@ -273,74 +273,6 @@ "color": "#432", "bgcolor": "#653" }, - { - "id": 42, - "type": "PreviewImage", - "pos": [ - -1911.866591853913, - -1189.5097461843127 - ], - "size": [ - 2324.0069677273495, - 581.7454612396214 - ], - "flags": {}, - "order": 16, - "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 205 - } - ], - "outputs": [], - "properties": { - "Node name for S&R": "PreviewImage" - }, - "widgets_values": [] - }, - { - "id": 39, - "type": "LoadImage", - "pos": [ - -1033.4287346830292, - 604.5107392434782 - ], - "size": [ - 412.12756911804206, - 508.84732322692867 - ], - "flags": {}, - "order": 2, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 206 - ] - }, - { - "name": "MASK", - "type": "MASK", - "links": null - } - ], - "properties": { - "Node name for S&R": "LoadImage", - "cnr_id": "comfy-core", - "ver": "0.14.1" - }, - "widgets_values": [ - "tmpbncl5y7d.png", - "image" - ], - "color": "#432", - "bgcolor": "#653" - }, { "id": 22, "type": "LoadImage", @@ -353,7 +285,7 @@ 508.84732322692867 ], "flags": {}, - "order": 3, + "order": 2, "mode": 0, "inputs": [], "outputs": [ @@ -394,8 +326,8 @@ 508.84732322692867 ], "flags": {}, - "order": 4, - "mode": 0, + "order": 3, + "mode": 4, "inputs": [], "outputs": [ { @@ -435,8 +367,8 @@ 508.84732322692867 ], "flags": {}, - "order": 5, - "mode": 0, + "order": 4, + "mode": 4, "inputs": [], "outputs": [ { @@ -476,8 +408,8 @@ 508.84732322692867 ], "flags": {}, - "order": 6, - "mode": 0, + "order": 5, + "mode": 4, "inputs": [], "outputs": [ { @@ -517,8 +449,8 @@ 508.84732322692867 ], "flags": {}, - "order": 7, - "mode": 0, + "order": 6, + "mode": 4, "inputs": [], "outputs": [ { @@ -558,8 +490,8 @@ 508.84732322692867 ], "flags": {}, - "order": 8, - "mode": 0, + "order": 7, + "mode": 4, "inputs": [], "outputs": [ { @@ -599,8 +531,8 @@ 508.84732322692867 ], "flags": {}, - "order": 9, - "mode": 0, + "order": 8, + "mode": 4, "inputs": [], "outputs": [ { @@ -640,8 +572,8 @@ 508.84732322692867 ], "flags": {}, - "order": 10, - "mode": 0, + "order": 9, + "mode": 4, "inputs": [], "outputs": [ { @@ -669,47 +601,6 @@ "color": "#432", "bgcolor": "#653" }, - { - "id": 18, - "type": "LoadImage", - "pos": [ - -1463.6929609110477, - -541.514916458471 - ], - "size": [ - 412.12756911804206, - 508.84732322692867 - ], - "flags": {}, - "order": 11, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 215 - ] - }, - { - "name": "MASK", - "type": "MASK", - "links": null - } - ], - "properties": { - "Node name for S&R": "LoadImage", - "cnr_id": "comfy-core", - "ver": "0.14.1" - }, - "widgets_values": [ - "348368277_936696344247420_4680049320151257292_n.jpg", - "image" - ], - "color": "#432", - "bgcolor": "#653" - }, { "id": 27, "type": "PrimerePreviewImage", @@ -748,6 +639,150 @@ null ] }, + { + "id": 9, + "type": "PrimitiveStringMultiline", + "pos": [ + 439.38399502500954, + -229.56518868321555 + ], + "size": [ + 675.7824161328128, + 338.0025676349712 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 190 + ] + } + ], + "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": 18, + "type": "LoadImage", + "pos": [ + -1463.6929609110477, + -541.514916458471 + ], + "size": [ + 412.12756911804206, + 508.84732322692867 + ], + "flags": {}, + "order": 11, + "mode": 4, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 215 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "Node name for S&R": "LoadImage", + "cnr_id": "comfy-core", + "ver": "0.14.1" + }, + "widgets_values": [ + "348368277_936696344247420_4680049320151257292_n.jpg", + "image" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 42, + "type": "PreviewImage", + "pos": [ + -1911.866591853913, + -1189.5097461843127 + ], + "size": [ + 2324.0069677273495, + 581.7454612396214 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 205 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PreviewImage" + }, + "widgets_values": [] + }, + { + "id": 39, + "type": "LoadImage", + "pos": [ + -1033.4287346830292, + 604.5107392434782 + ], + "size": [ + 412.12756911804206, + 508.84732322692867 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 206 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "Node name for S&R": "LoadImage", + "cnr_id": "comfy-core", + "ver": "0.14.1" + }, + "widgets_values": [ + "tmpbncl5y7d.png", + "image" + ], + "color": "#432", + "bgcolor": "#653" + }, { "id": 17, "type": "PrimereMultiImage", @@ -877,41 +912,6 @@ "color": "#432", "bgcolor": "#653" }, - { - "id": 9, - "type": "PrimitiveStringMultiline", - "pos": [ - 439.38399502500954, - -229.56518868321555 - ], - "size": [ - 675.7824161328128, - 338.0025676349712 - ], - "flags": {}, - "order": 12, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 190 - ] - } - ], - "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": 15, "type": "PrimereResolution", @@ -981,7 +981,7 @@ }, "widgets_values": [ "HD+ screen [16:10]", - false, + true, 1024, false, "Horizontal", @@ -1001,7 +1001,7 @@ -548.3233034269158 ], "size": [ - 670.1635931396486, + 648.737226759225, 482 ], "flags": {}, @@ -1345,10 +1345,10 @@ "config": {}, "extra": { "ds": { - "scale": 0.5644739300537774, + "scale": 0.5644739300537776, "offset": [ - 1409.1471240538765, - 958.9479125085975 + 1803.7626251739487, + 803.8351710145441 ] }, "frontendVersion": "1.39.19" diff --git a/components/API/api_schema_registry.py b/components/API/api_schema_registry.py index 4d2f12d..874f3e3 100644 --- a/components/API/api_schema_registry.py +++ b/components/API/api_schema_registry.py @@ -2,6 +2,8 @@ from __future__ import annotations from copy import deepcopy from typing import Any +from pathlib import Path +import json def _is_service_schema(node: Any) -> bool: @@ -55,4 +57,85 @@ def get_schema( return deepcopy(service_map[service]), service first_service = next(iter(service_map.keys())) - return deepcopy(service_map[first_service]), first_service \ No newline at end of file + return deepcopy(service_map[first_service]), first_service + +def _load_json_file(file_path: str, label: str) -> Any: + path = str(file_path or "").strip() + if not path: + raise RuntimeError(f"Missing path for {label} JSON file") + if not Path(path).exists(): + raise RuntimeError(f"{label} JSON file not found: {path}") + + try: + return json.loads(Path(path).read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid JSON syntax in {label} file '{path}' at line {exc.lineno}, column {exc.colno}: {exc.msg}") from exc + + +def load_and_validate_api_schema_registry(schema_path: str, apiconfig_path: str | None = None) -> dict[str, dict[str, dict[str, Any]]]: + schema_data = _load_json_file(schema_path, "API schema registry") + if not isinstance(schema_data, dict): + raise RuntimeError("API schema registry root must be a JSON object mapping providers to services.") + + config_path = apiconfig_path or os.path.join(PRIMERE_ROOT, "json", "apiconfig.example.json") + apiconfig_data = _load_json_file(config_path, "API config") + if not isinstance(apiconfig_data, dict): + raise RuntimeError("API config root must be a JSON object mapping provider names.") + + allowed_providers = {str(name).strip() for name in apiconfig_data.keys() if str(name).strip()} + if len(allowed_providers) == 0: + raise RuntimeError("API config does not define any provider names.") + + validated: dict[str, dict[str, dict[str, Any]]] = {} + + for provider_key, services in schema_data.items(): + provider_name = str(provider_key).strip() + if not provider_name: + raise RuntimeError("Schema provider key cannot be empty.") + if provider_name not in allowed_providers: + allowed = ", ".join(sorted(allowed_providers)) + raise RuntimeError(f"Provider '{provider_name}' in API schema is not registered in API config '{config_path}'. Allowed providers: {allowed}") + if not isinstance(services, dict): + raise RuntimeError(f"Provider '{provider_name}' value must be an object mapping services to schemas.") + + validated_services: dict[str, dict[str, Any]] = {} + for service_key, schema in services.items(): + service_name = str(service_key).strip() + if not service_name: + raise RuntimeError(f"Provider '{provider_name}' contains an empty service key.") + if not isinstance(schema, dict): + raise RuntimeError(f"Schema for provider '{provider_name}' service '{service_name}' must be a JSON object.") + + inner_provider = str(schema.get("provider") or "").strip() + inner_service = str(schema.get("service") or "").strip() + if inner_provider != provider_name: + raise RuntimeError(f"Provider/service mismatch at '{provider_name}/{service_name}': schema field 'provider' must equal '{provider_name}', got '{inner_provider or ''}'.") + if inner_service != service_name: + raise RuntimeError(f"Provider/service mismatch at '{provider_name}/{service_name}': schema field 'service' must equal '{service_name}', got '{inner_service or ''}'.") + + request = schema.get("request") + if not isinstance(request, dict): + raise RuntimeError(f"Schema '{provider_name}/{service_name}' must contain object key 'request'.") + + method = str(request.get("method") or "").strip().upper() + if not method: + raise RuntimeError(f"Schema '{provider_name}/{service_name}' request.method is required.") + endpoint = str(request.get("endpoint") or "").strip() + if not endpoint: + raise RuntimeError(f"Schema '{provider_name}/{service_name}' request.endpoint is required.") + + possible_parameters = schema.get("possible_parameters", {}) + if not isinstance(possible_parameters, dict): + raise RuntimeError(f"Schema '{provider_name}/{service_name}' key 'possible_parameters' must be an object.") + + import_modules = schema.get("import_modules", []) + if not isinstance(import_modules, list): + raise RuntimeError(f"Schema '{provider_name}/{service_name}' key 'import_modules' must be a list of import statements." ) + for idx, import_line in enumerate(import_modules): + if not isinstance(import_line, str) or not import_line.strip(): + raise RuntimeError(f"Schema '{provider_name}/{service_name}' import_modules[{idx}] must be a non-empty string.") + + validated_services[service_name] = schema + validated[provider_name] = validated_services + + return validated \ No newline at end of file diff --git a/components/API/external_api_backend.py b/components/API/external_api_backend.py index 31f90bb..ec83330 100644 --- a/components/API/external_api_backend.py +++ b/components/API/external_api_backend.py @@ -5,6 +5,7 @@ import re import sys import importlib import os +import json from dataclasses import dataclass from typing import Any from pathlib import Path @@ -21,7 +22,6 @@ PLACEHOLDER_RE = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}") class ExternalAPIError(RuntimeError): pass - @dataclass class RenderResult: provider: str @@ -155,6 +155,49 @@ def build_sdk_context(rendered: RenderResult, client: Any) -> tuple[dict[str, An return context, allowed_roots +def load_import_modules(import_modules: list[str] | None) -> tuple[dict[str, Any], set[str]]: + """Load schema-defined imports into SDK execution context.""" + context: dict[str, Any] = {} + allowed_roots: set[str] = set() + + for import_line in import_modules or []: + if not isinstance(import_line, str): + continue + line = import_line.strip() + if not line: + continue + + if line.startswith("import "): + module_specs = [part.strip() for part in line[len("import "):].split(",") if part.strip()] + for spec in module_specs: + if " as " in spec: + module_name, alias = [part.strip() for part in spec.split(" as ", 1)] + else: + module_name = spec + alias = module_name.split(".")[-1] + module_obj = importlib.import_module(module_name) + context[alias] = module_obj + allowed_roots.add(alias) + continue + + if line.startswith("from ") and " import " in line: + module_name, imported = line[len("from "):].split(" import ", 1) + module_name = module_name.strip() + module_obj = importlib.import_module(module_name) + symbol_specs = [part.strip() for part in imported.split(",") if part.strip()] + for spec in symbol_specs: + if " as " in spec: + symbol_name, alias = [part.strip() for part in spec.split(" as ", 1)] + else: + symbol_name = spec + alias = symbol_name + context[alias] = getattr(module_obj, symbol_name) + allowed_roots.add(alias) + continue + + raise ExternalAPIError(f"Unsupported import syntax in schema import_modules: {import_line}") + + return context, allowed_roots def _resolve_dotted_from_context(path: str, context: dict[str, Any], allowed_roots: set[str]) -> Any: if not path: @@ -320,31 +363,73 @@ def parse_ratio(value): if denominator == 0: return None return numerator / denominator + +def _parse_ratio_parts(value: Any) -> tuple[float, float] | None: + text = str(value).strip() if value is not None else "" + if ":" not in text: + return None + + left, right = text.split(":", 1) + try: + a = float(left.strip()) + b = float(right.strip()) + except ValueError: + return None + + if a <= 0 or b <= 0: + return None + return a, b + + +def _ratio_orientation(a: float, b: float) -> str: + if a > b: + return "horizontal" + if a < b: + return "vertical" + return "square" + def closest_valid_ratio(value, valid_ratios): - if not isinstance(valid_ratios, list) or len(valid_ratios) == 0: + if not isinstance(valid_ratios, (list, tuple)) or len(valid_ratios) == 0: return value - normalized_valid = [str(ratio) for ratio in valid_ratios] candidate = str(value).strip() if value is not None else "" + normalized_valid = [str(ratio).strip() for ratio in valid_ratios if str(ratio).strip()] + if len(normalized_valid) == 0: + return value if candidate in normalized_valid: return candidate - candidate_ratio = parse_ratio(candidate) - if candidate_ratio is None: + candidate_parts = _parse_ratio_parts(candidate) + if candidate_parts is None: return normalized_valid[0] - best_value = normalized_valid[0] - best_diff = float("inf") - for ratio_text in normalized_valid: - parsed_ratio = parse_ratio(ratio_text) - if parsed_ratio is None: - continue - diff = abs(parsed_ratio - candidate_ratio) - if diff < best_diff: - best_diff = diff - best_value = ratio_text + input_a, input_b = candidate_parts + input_product = input_a * input_b + input_orientation = _ratio_orientation(input_a, input_b) - return best_value + same_orientation_matches: list[tuple[str, float]] = [] + fallback_matches: list[tuple[str, float]] = [] + + for ratio_text in normalized_valid: + ratio_parts = _parse_ratio_parts(ratio_text) + if ratio_parts is None: + continue + + valid_a, valid_b = ratio_parts + valid_product = valid_a * valid_b + product_diff = abs(valid_product - input_product) + valid_orientation = _ratio_orientation(valid_a, valid_b) + + fallback_matches.append((ratio_text, product_diff)) + if valid_orientation == input_orientation: + same_orientation_matches.append((ratio_text, product_diff)) + + pool = same_orientation_matches if len(same_orientation_matches) > 0 else fallback_matches + if len(pool) == 0: + return normalized_valid[0] + + pool.sort(key=lambda item: item[1]) + return pool[0][0] def _safe_response_handler_filename(name: str) -> str: filename = str(name or "").strip() diff --git a/front_end/api_schemas.json b/front_end/api_schemas.json index 22368bf..c570e1b 100644 --- a/front_end/api_schemas.json +++ b/front_end/api_schemas.json @@ -4,6 +4,10 @@ "provider": "Gemini", "service": "Nanobanana_V1", "response_handler": "Gemini_Nanobanana.py", + "import_modules": [ + "from google import genai", + "from google.genai import types" + ], "possible_parameters": { "aspect_ratio": ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"], "resolution": ["1K", "2K", "4K"], @@ -45,6 +49,10 @@ "provider": "Gemini", "service": "Nanobanana_V2", "response_handler": "Gemini_Nanobanana.py", + "import_modules": [ + "from google import genai", + "from google.genai import types" + ], "possible_parameters": { "aspect_ratio": ["1:1","1:4","1:8","2:3","3:2","3:4","4:1","4:3","4:5","5:4","8:1","9:16","16:9","21:9"], "resolution": ["512px", "1K", "2K", "4K"], @@ -121,6 +129,10 @@ "Imagen": { "provider": "Gemini", "service": "Imagen", + "import_modules": [ + "from google import genai", + "from google.genai import types" + ], "possible_parameters": { "aspect_ratio": ["1:1", "3:4", "4:3", "9:16", "16:9"], "resolution": ["1K", "2K"], diff --git a/terminal_helpers/api_snippet_to_json.py b/terminal_helpers/api_snippet_to_json.py index c8e848f..a405e98 100644 --- a/terminal_helpers/api_snippet_to_json.py +++ b/terminal_helpers/api_snippet_to_json.py @@ -33,6 +33,12 @@ KNOWN_PARAM_OPTIONS: dict[str, list[str]] = { EXCLUDED_PARAMETER_KEYS = {"prompt", "response_modalities", "width", "height", "seed", "reference_images", "first_image", "last_image", "negative_prompt"} +DEFAULT_IMPORT_MODULES: dict[str, list[str]] = { + "generic": [ + "import your_provider_sdk", + "from your_provider_sdk import types", + ] +} def dotted_name(node: ast.AST) -> str: if isinstance(node, ast.Name): @@ -178,6 +184,35 @@ def build_possible_parameters(request_schema: dict[str, Any]) -> dict[str, list[ return possible +def build_import_modules(snippet: str, provider: str = "") -> list[str]: + """Extract import statements so schema keeps service-specific dependencies editable.""" + tree = ast.parse(snippet) + imports: list[str] = [] + + for node in tree.body: + if isinstance(node, ast.Import): + rendered = ", ".join( + f"{alias.name} as {alias.asname}" if alias.asname else alias.name + for alias in node.names + ) + imports.append(f"import {rendered}") + elif isinstance(node, ast.ImportFrom): + module_name = "." * node.level + (node.module or "") + rendered = ", ".join( + f"{alias.name} as {alias.asname}" if alias.asname else alias.name + for alias in node.names + ) + imports.append(f"from {module_name} import {rendered}") + + # Preserve order while dropping accidental duplicates. + unique_imports = list(dict.fromkeys(imports)) + if unique_imports: + return unique_imports + + provider_key = str(provider or "").strip().lower() + if provider_key in DEFAULT_IMPORT_MODULES: + return DEFAULT_IMPORT_MODULES[provider_key] + return DEFAULT_IMPORT_MODULES["generic"] def build_service_schema(snippet: str, provider: str = DEFAULT_PROVIDER, service: str = DEFAULT_SERVICE) -> dict[str, Any]: tree = ast.parse(snippet) @@ -203,6 +238,7 @@ def build_service_schema(snippet: str, provider: str = DEFAULT_PROVIDER, service "provider": provider, "service": service, "response_handler": response_handler_filename(provider, service), + "import_modules": build_import_modules(snippet, provider=provider), "possible_parameters": build_possible_parameters(request_schema), "request": request_schema, } diff --git a/terminal_helpers/result.json b/terminal_helpers/result.json index 908f84c..9343824 100644 --- a/terminal_helpers/result.json +++ b/terminal_helpers/result.json @@ -1,46 +1,41 @@ { - "BlackForest": { - "FluxExpandPro": { - "provider": "BlackForest", - "service": "FluxExpandPro", - "response_handler": "BlackForest_FluxExpandPro.py", - "possible_parameters": { - "aspect_ratio": [ - "fake_aspect_ratio_value_1", - "fake_aspect_ratio_value_2" - ], - "mask_images": [ - "fake_mask_images_value_1", - "fake_mask_images_value_2" - ], - "output_format": [ - "fake_output_format_value_1", - "fake_output_format_value_2" - ], - "safety_tolerance": [ - "fake_safety_tolerance_value_1", - "fake_safety_tolerance_value_2" - ] - }, + "Gemini": { + "Nanobanana": { + "provider": "Gemini", + "service": "Nanobanana", + "response_handler": "Gemini_Nanobanana.py", + "import_modules": [ + "from google import genai", + "from google.genai import types" + ], + "possible_parameters": {}, "request": { "method": "SDK", - "endpoint": "requests.post", + "endpoint": "client.models.generate_content", "sdk_call": { - "args": [ - "{{arg0}}" - ], + "args": [], "kwargs": { - "payload": { - "output_format": "{{output_format}}", - "image": "{{reference_images}}", - "mask": "{{mask_images}}", - "safety_tolerance": "{{safety_tolerance}}", - "prompt": "{{prompt}}", - "seed": "{{seed}}", - "aspect_ratio": "{{aspect_ratio}}", - "prompt_upsampling": "STRING", - "guidance": "FLOAT", - "steps": "INT" + "model": "gemini-3-pro-image-preview", + "contents": [ + "{{prompt}}", + "{{reference_images}}" + ], + "config": { + "$call": "types.GenerateContentConfig", + "$args": [], + "$kwargs": { + "response_modalities": [ + "IMAGE" + ], + "image_config": { + "$call": "types.ImageConfig", + "$args": [], + "$kwargs": { + "aspect_ratio": "16:9", + "image_size": "1K" + } + } + } } } } diff --git a/terminal_helpers/snippet.py b/terminal_helpers/snippet.py index 3af6e9b..df8c185 100644 --- a/terminal_helpers/snippet.py +++ b/terminal_helpers/snippet.py @@ -1,14 +1,11 @@ -response = requests.post("https://api.bfl.ai/v1/flux-pro-1.0-fill", - payload={ - "output_format": "png", - "image": reference_images, - "mask": mask_images, - "safety_tolerance": safety_tolerance, - "prompt": prompt, - "seed": seed, - "aspect_ratio": aspect_ratio, - "prompt_upsampling": "STRING", - "guidance": "FLOAT", - "steps": "INT" - } +response = client.models.generate_content( + model="gemini-3-pro-image-preview", + contents=[prompt, reference_images], + config=types.GenerateContentConfig( + response_modalities=["IMAGE"], + image_config=types.ImageConfig( + aspect_ratio="16:9", + image_size="1K", + ) + ) ) \ No newline at end of file