V 2.0.0 - Universal api #45 - BFL
This commit is contained in:
+24
-1
@@ -127,6 +127,20 @@ class PrimereApiProcessor:
|
||||
if api_provider == "Gemini":
|
||||
gemini_image_data = Image.open(TEMP_FILE_REF)
|
||||
img_binary_api.append(gemini_image_data)
|
||||
if api_provider == "BlackForest":
|
||||
encoded_image = None
|
||||
single_image = source_images[0]
|
||||
width_original = single_image.shape[2]
|
||||
height_original = single_image.shape[1]
|
||||
image_np_bf = (single_image.numpy() * 255).astype(np.uint8)
|
||||
img_bf = Image.fromarray(image_np_bf)
|
||||
img_byte_arr_bf = io.BytesIO()
|
||||
img_bf.save(img_byte_arr_bf, format="PNG")
|
||||
img_byte_arr_bf.seek(0)
|
||||
encoded_string = base64.b64encode(img_byte_arr_bf.read())
|
||||
img_binary_api = encoded_string.decode('ascii')
|
||||
break
|
||||
|
||||
elif hasattr(loaded_client_for_upload, "upload_file"):
|
||||
uploaded_reference = loaded_client_for_upload.upload_file(TEMP_FILE_REF)
|
||||
img_binary_api.append(uploaded_reference)
|
||||
@@ -200,11 +214,20 @@ class PrimereApiProcessor:
|
||||
|
||||
try:
|
||||
if rendered.method.upper() == "SDK":
|
||||
context = {"client": client}
|
||||
provider_config = config_json.get(api_provider, {}) if isinstance(config_json, dict) else {}
|
||||
provider_api_key = provider_config.get("APIKEY") if isinstance(provider_config, dict) else None
|
||||
context = {"client": client, "provider_api_key": provider_api_key}
|
||||
allowed_roots = {"client"}
|
||||
imported_context, imported_roots = external_api_backend.load_import_modules(schema_import_modules)
|
||||
context.update(imported_context)
|
||||
allowed_roots.update(imported_roots)
|
||||
|
||||
auto_context, auto_roots = external_api_backend.build_sdk_context(rendered, client)
|
||||
for root_name in auto_roots:
|
||||
if root_name not in context and root_name in auto_context:
|
||||
context[root_name] = auto_context[root_name]
|
||||
allowed_roots.add(root_name)
|
||||
|
||||
sdk_context = dict(context)
|
||||
sdk_call_data = rendered.sdk_call if isinstance(rendered.sdk_call, dict) else {}
|
||||
sdk_args = sdk_call_data.get("args", []) if isinstance(sdk_call_data, dict) else []
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
from typing import Any
|
||||
import os
|
||||
from . import external_api_backend
|
||||
from . import api_helper
|
||||
|
||||
def _canonical_param_name(name: str) -> str:
|
||||
low = name.lower()
|
||||
@@ -10,12 +12,62 @@ def _canonical_param_name(name: str) -> str:
|
||||
return "resolution"
|
||||
if low == "model" or low.endswith("_model"):
|
||||
return "model"
|
||||
if "prompt" in low or "contents" in low:
|
||||
if low in {"prompt", "contents"} or low.endswith("_prompt"):
|
||||
return "prompt"
|
||||
if "response_modalities" in low:
|
||||
return "response_modalities"
|
||||
return name
|
||||
|
||||
def _provider_api_key(spec: dict[str, Any]) -> Any:
|
||||
provider = str(spec.get("provider") or "").strip()
|
||||
if not provider:
|
||||
return None
|
||||
try:
|
||||
config = api_helper.get_api_config("apiconfig.json")
|
||||
except Exception:
|
||||
return None
|
||||
entry = config.get(provider, {}) if isinstance(config, dict) else {}
|
||||
if not isinstance(entry, dict):
|
||||
return None
|
||||
value = entry.get("APIKEY")
|
||||
return value if value not in (None, "") else None
|
||||
|
||||
|
||||
def _secret_placeholder_default(key: str, spec: dict[str, Any]) -> Any:
|
||||
normalized = str(key or "").strip()
|
||||
low = normalized.lower()
|
||||
|
||||
# If placeholder itself looks like an env-var token, keep token name.
|
||||
# This preserves schemas that intentionally use:
|
||||
# {"$call": "os.environ.get", "$args": ["{{BFL_API_KEY}}"]}
|
||||
# so runtime executes os.environ.get("BFL_API_KEY") correctly.
|
||||
if normalized.upper() == normalized and "_" in normalized:
|
||||
if low.endswith(("_key", "_token", "_secret", "_password")):
|
||||
return normalized
|
||||
|
||||
direct_env = os.environ.get(normalized) or os.environ.get(normalized.upper())
|
||||
if direct_env not in (None, ""):
|
||||
return direct_env
|
||||
|
||||
if low.endswith("_api_key") or low in {"api_key", "apikey", "x_api_key", "provider_api_key", "authorization", "auth_token", "access_token", "bearer_token", "token"}:
|
||||
return _provider_api_key(spec)
|
||||
|
||||
return None
|
||||
|
||||
def _marker_default(marker: Any) -> Any:
|
||||
if not isinstance(marker, str):
|
||||
return None
|
||||
value = marker.strip().upper()
|
||||
if value == "INT":
|
||||
return 1
|
||||
if value == "FLOAT":
|
||||
return 1.0
|
||||
if value == "STRING":
|
||||
return ""
|
||||
if value == "BOOLEAN":
|
||||
return False
|
||||
return None
|
||||
|
||||
def _default_value(name: str) -> Any:
|
||||
k = name.lower()
|
||||
if "prompt" in k:
|
||||
@@ -62,7 +114,14 @@ def _build_values(spec: dict[str, Any], values: dict[str, Any] | None = None) ->
|
||||
elif canonical in possible_parameters and isinstance(possible_parameters[canonical], list) and len(possible_parameters[canonical]) > 0:
|
||||
selected = possible_parameters[canonical][0]
|
||||
else:
|
||||
selected = _default_value(canonical)
|
||||
secret_selected = _secret_placeholder_default(key, spec)
|
||||
if secret_selected is not None:
|
||||
selected = secret_selected
|
||||
else:
|
||||
marker_selected = _marker_default(possible_parameters.get(key)) if key in possible_parameters else None
|
||||
if marker_selected is None and canonical in possible_parameters:
|
||||
marker_selected = _marker_default(possible_parameters.get(canonical))
|
||||
selected = marker_selected if marker_selected is not None else _default_value(canonical)
|
||||
|
||||
resolved[key] = selected
|
||||
|
||||
|
||||
@@ -229,6 +229,20 @@ def _materialize_sdk_value(value: Any, context: dict[str, Any], allowed_roots: s
|
||||
return {k: _materialize_sdk_value(v, context, allowed_roots) for k, v in value.items()}
|
||||
return value
|
||||
|
||||
def _apply_auth_header_fallback(kwargs: dict[str, Any], context: dict[str, Any]) -> None:
|
||||
headers = kwargs.get("headers") if isinstance(kwargs, dict) else None
|
||||
if not isinstance(headers, dict):
|
||||
return
|
||||
|
||||
provider_api_key = context.get("provider_api_key")
|
||||
if provider_api_key in (None, ""):
|
||||
return
|
||||
|
||||
for auth_key in ("x-key", "x_api_key", "api-key", "authorization", "Authorization"):
|
||||
if auth_key not in headers:
|
||||
continue
|
||||
if headers.get(auth_key) in (None, "", "null"):
|
||||
headers[auth_key] = provider_api_key
|
||||
|
||||
def execute_sdk_request(rendered: RenderResult, context: dict[str, Any], allowed_roots: set[str] | None = None) -> Any:
|
||||
if rendered.method.upper() != "SDK":
|
||||
@@ -238,6 +252,7 @@ def execute_sdk_request(rendered: RenderResult, context: dict[str, Any], allowed
|
||||
args, kwargs = normalize_sdk_call(rendered.sdk_call)
|
||||
safe_args = [_materialize_sdk_value(a, context, roots) for a in args]
|
||||
safe_kwargs = {k: _materialize_sdk_value(v, context, roots) for k, v in kwargs.items()}
|
||||
_apply_auth_header_fallback(safe_kwargs, context)
|
||||
return fn(*safe_args, **safe_kwargs)
|
||||
|
||||
def default_provider_service(node_data):
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
import json
|
||||
import requests
|
||||
import comfy.utils
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
import fal_client
|
||||
from io import BytesIO
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
def handle_response(api_result, schema=None, loaded_client=None, response_url=None):
|
||||
result_image = None
|
||||
image_list = []
|
||||
final_batch_img = []
|
||||
status_accepted = ['Ready', 'Pending', 'Task not found']
|
||||
parsed_url = urlparse(response_url)
|
||||
blackforest_api_region = parsed_url.netloc or None
|
||||
path_parts = [part for part in parsed_url.path.split("/") if part]
|
||||
blackforest_api_version = path_parts[0] if len(path_parts) > 0 else None
|
||||
|
||||
try:
|
||||
json_object = json.loads(api_result.text)
|
||||
except ValueError as e:
|
||||
raise RuntimeError(f"Input object failed: {api_result}")
|
||||
|
||||
if 'polling_url' in json_object:
|
||||
resp = requests.get(json_object['polling_url'])
|
||||
resp_json_object = json.loads(resp.text)
|
||||
|
||||
status = 'Start'
|
||||
request_tryout = 0
|
||||
error_tryout = 0
|
||||
|
||||
while error_tryout <= 1:
|
||||
while status != 'Ready' or request_tryout <= 20:
|
||||
url_res = f"https://{blackforest_api_region}/{blackforest_api_version}/get_result"
|
||||
querystring = {"id": resp_json_object['id']}
|
||||
response = requests.request("GET", url_res, params=querystring)
|
||||
resp_json_object = json.loads(response.text)
|
||||
status = resp_json_object['status']
|
||||
if status not in status_accepted:
|
||||
resp_error = requests.get(json_object['polling_url'])
|
||||
resp_error_json_object = json.loads(resp_error.text)
|
||||
error_status = resp_error_json_object['status']
|
||||
raise RuntimeError(f"Response status: {status}, error status: {error_status}")
|
||||
if status == 'Ready':
|
||||
break
|
||||
time.sleep(2)
|
||||
request_tryout = request_tryout + 1
|
||||
time.sleep(1)
|
||||
if status == 'Ready':
|
||||
break
|
||||
error_tryout = error_tryout + 1
|
||||
|
||||
if status == 'Ready':
|
||||
image_url = resp_json_object['result']['sample']
|
||||
response = requests.get(image_url)
|
||||
result_image = Image.open(BytesIO(response.content))
|
||||
if result_image is not None:
|
||||
result_image = result_image.convert("RGB")
|
||||
result_image = np.array(result_image).astype(np.float32) / 255.0
|
||||
result_image = torch.from_numpy(result_image)[None,]
|
||||
final_batch_img.append(result_image)
|
||||
else:
|
||||
raise RuntimeError(f"No result image...")
|
||||
else:
|
||||
raise RuntimeError(f"No polling_url in response: {json_object}")
|
||||
|
||||
if type(final_batch_img).__name__ == "list" and len(final_batch_img) > 1:
|
||||
image_list = final_batch_img
|
||||
single_image_start = final_batch_img[0]
|
||||
batch_count = 0
|
||||
s = None
|
||||
for single_image in final_batch_img:
|
||||
if (batch_count + 1) < len(final_batch_img):
|
||||
current_single_image = final_batch_img[batch_count + 1]
|
||||
if single_image_start.shape[1:] != current_single_image.shape[1:]:
|
||||
current_single_image = comfy.utils.common_upscale(current_single_image.movedim(-1, 1), single_image_start.shape[2], single_image_start.shape[1], "bilinear", "center").movedim(1, -1)
|
||||
batch_count = batch_count + 1
|
||||
if s is not None:
|
||||
single_image = s
|
||||
s = torch.cat((current_single_image, single_image), dim=0)
|
||||
result_image = s
|
||||
|
||||
return result_image
|
||||
@@ -385,14 +385,17 @@
|
||||
}
|
||||
},
|
||||
"BlackForest": {
|
||||
"flux-2-klein-4b": {
|
||||
"Flux": {
|
||||
"provider": "BlackForest",
|
||||
"service": "flux-2-klein-4b",
|
||||
"service": "Flux",
|
||||
"response_handler": "Blackforest_Image.py",
|
||||
"import_modules": [
|
||||
],
|
||||
"possible_parameters": {
|
||||
"safety_tolerance": [0, 1, 2, 3, 4, 5],
|
||||
"model_name": ["flux-kontext-pro", "flux-kontext-max", "flux-pro-1.0-fill", "flux-pro-1.0-expand", "flux-pro", "flux-pro-1.1", "flux-pro-1.1-ultra", "flux-2-flex", "flux-2-pro", "flux-2-max", "flux-2-klein-4b", "flux-2-klein-9b"],
|
||||
"regions": ["api.bfl.ai", "api.eu.bfl.ai", "api.us.bfl.ai"],
|
||||
"version": ["v1"],
|
||||
"safety_tolerance": [0, 1, 2, 3, 4, 5, 6],
|
||||
"prompt_upsampling": [false, true],
|
||||
"guidance": "FLOAT",
|
||||
"steps": "INT"
|
||||
@@ -402,23 +405,20 @@
|
||||
"endpoint": "requests.post",
|
||||
"sdk_call": {
|
||||
"args": [
|
||||
"https://api.bfl.ai/v1/flux-2-klein-4b"
|
||||
"https://{{regions}}/{{version}}/{{model_name}}"
|
||||
],
|
||||
"kwargs": {
|
||||
"headers": {
|
||||
"accept": "application/json",
|
||||
"x-key": {
|
||||
"$call": "os.environ.get",
|
||||
"$args": [
|
||||
"{{BFL_API_KEY}}"
|
||||
],
|
||||
"$args": ["{{BFL_API_KEY}}"],
|
||||
"$kwargs": {}
|
||||
},
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"json": {
|
||||
"output_format": "png",
|
||||
"image_prompt": "{{reference_images}}",
|
||||
"safety_tolerance": "{{safety_tolerance}}",
|
||||
"prompt": "{{prompt}}",
|
||||
"seed": "{{seed}}",
|
||||
|
||||
@@ -29,8 +29,11 @@ PLACEHOLDER_ALIASES = {"number_of_images": "seed", "aspectRatio": "aspect_ratio"
|
||||
KNOWN_PARAM_OPTIONS: dict[str, list[str]] = {
|
||||
"model": ["example-model-1", "example-model-2"],
|
||||
"resolution": ["1K", "2K", "4K"],
|
||||
"regions": ["api.bfl.ai", "api.eu.bfl.ai", "api.us.bfl.ai"],
|
||||
}
|
||||
|
||||
TYPE_MARKERS = {"INT", "FLOAT", "STRING", "BOOLEAN"}
|
||||
INLINE_PLACEHOLDER_RE = re.compile(r"(?<!\{)\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}(?!\})")
|
||||
EXCLUDED_PARAMETER_KEYS = {"prompt", "response_modalities", "width", "height", "seed", "reference_images", "first_image", "last_image", "negative_prompt"}
|
||||
|
||||
DEFAULT_IMPORT_MODULES: dict[str, list[str]] = {
|
||||
@@ -56,14 +59,18 @@ def placeholder(name: str) -> str:
|
||||
clean = clean.replace("__", "_")
|
||||
return f"{{{{{clean.strip('_') or 'value'}}}}}"
|
||||
|
||||
def normalize_inline_placeholders(value: str) -> str:
|
||||
return INLINE_PLACEHOLDER_RE.sub(lambda m: placeholder(m.group(1)), value)
|
||||
|
||||
|
||||
def node_to_template(node: ast.AST, path: str) -> Any:
|
||||
if isinstance(node, ast.Constant):
|
||||
if node.value is None or isinstance(node.value, bool):
|
||||
return node.value
|
||||
if isinstance(node.value, str):
|
||||
if node.value in {"INT", "FLOAT", "STRING"}:
|
||||
if node.value in TYPE_MARKERS:
|
||||
return placeholder(path)
|
||||
return normalize_inline_placeholders(node.value)
|
||||
return node.value
|
||||
|
||||
if isinstance(node, ast.Name):
|
||||
@@ -136,7 +143,7 @@ def _collect_placeholders(node: Any) -> set[str]:
|
||||
def walk(item: Any) -> None:
|
||||
if isinstance(item, dict):
|
||||
for k, v in item.items():
|
||||
if k in {"$args", "args"}:
|
||||
if k in {"$args"}:
|
||||
continue
|
||||
walk(v)
|
||||
return
|
||||
@@ -162,21 +169,59 @@ def _canonical_param_name(name: str) -> str:
|
||||
return "model"
|
||||
if low == "number_of_images":
|
||||
return "seed"
|
||||
if "prompt" in low or "contents" in low:
|
||||
if low in {"prompt", "contents"} or low.endswith("_prompt"):
|
||||
return "prompt"
|
||||
if "response_modalities" in low:
|
||||
return "response_modalities"
|
||||
return name
|
||||
|
||||
def _collect_type_markers(node: ast.AST) -> dict[str, str]:
|
||||
marked: dict[str, str] = {}
|
||||
|
||||
def build_possible_parameters(request_schema: dict[str, Any]) -> dict[str, list[str]]:
|
||||
def walk(item: ast.AST) -> None:
|
||||
if isinstance(item, ast.Dict):
|
||||
for key_node, value_node in zip(item.keys, item.values):
|
||||
if isinstance(key_node, ast.Constant) and isinstance(key_node.value, str):
|
||||
key = key_node.value
|
||||
else:
|
||||
key = ast.unparse(key_node)
|
||||
|
||||
if isinstance(value_node, ast.Constant) and isinstance(value_node.value, str) and value_node.value in TYPE_MARKERS:
|
||||
marked[key] = value_node.value
|
||||
|
||||
walk(value_node)
|
||||
return
|
||||
|
||||
if isinstance(item, (ast.List, ast.Tuple, ast.Set)):
|
||||
for child in item.elts:
|
||||
walk(child)
|
||||
return
|
||||
|
||||
if isinstance(item, ast.Call):
|
||||
for child in item.args:
|
||||
walk(child)
|
||||
for kw in item.keywords:
|
||||
walk(kw.value)
|
||||
|
||||
walk(node)
|
||||
return marked
|
||||
|
||||
def build_possible_parameters(request_schema: dict[str, Any], type_markers: dict[str, str] | None = None) -> dict[str, Any]:
|
||||
placeholders = sorted(_collect_placeholders(request_schema))
|
||||
possible: dict[str, list[str]] = {}
|
||||
possible: dict[str, Any] = {}
|
||||
marker_map = {_canonical_param_name(k): v for k, v in (type_markers or {}).items()}
|
||||
|
||||
for name in placeholders:
|
||||
canonical = _canonical_param_name(name)
|
||||
if canonical in EXCLUDED_PARAMETER_KEYS:
|
||||
continue
|
||||
marker = marker_map.get(canonical)
|
||||
if marker == "BOOLEAN":
|
||||
possible[canonical] = [False, True]
|
||||
continue
|
||||
if marker in {"INT", "FLOAT", "STRING"}:
|
||||
possible[canonical] = marker
|
||||
continue
|
||||
if canonical in KNOWN_PARAM_OPTIONS:
|
||||
possible[canonical] = KNOWN_PARAM_OPTIONS[canonical]
|
||||
elif canonical not in possible:
|
||||
@@ -234,12 +279,14 @@ def build_service_schema(snippet: str, provider: str = DEFAULT_PROVIDER, service
|
||||
},
|
||||
}
|
||||
|
||||
type_markers = _collect_type_markers(call)
|
||||
|
||||
service_schema = {
|
||||
"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),
|
||||
"possible_parameters": build_possible_parameters(request_schema, type_markers=type_markers),
|
||||
"request": request_schema,
|
||||
}
|
||||
|
||||
|
||||
@@ -9,17 +9,28 @@
|
||||
"from your_provider_sdk import types"
|
||||
],
|
||||
"possible_parameters": {
|
||||
"guidance": [
|
||||
"fake_guidance_value_1",
|
||||
"fake_guidance_value_2"
|
||||
"guidance": "FLOAT",
|
||||
"model_name": [
|
||||
"fake_model_name_value_1",
|
||||
"fake_model_name_value_2"
|
||||
],
|
||||
"prompt_upsampling": [
|
||||
false,
|
||||
true
|
||||
],
|
||||
"regions": [
|
||||
"api.bfl.ai",
|
||||
"api.eu.bfl.ai",
|
||||
"api.us.bfl.ai"
|
||||
],
|
||||
"safety_tolerance": [
|
||||
"fake_safety_tolerance_value_1",
|
||||
"fake_safety_tolerance_value_2"
|
||||
],
|
||||
"steps": [
|
||||
"fake_steps_value_1",
|
||||
"fake_steps_value_2"
|
||||
"steps": "INT",
|
||||
"version": [
|
||||
"fake_version_value_1",
|
||||
"fake_version_value_2"
|
||||
]
|
||||
},
|
||||
"request": {
|
||||
@@ -27,7 +38,7 @@
|
||||
"endpoint": "requests.post",
|
||||
"sdk_call": {
|
||||
"args": [
|
||||
"https://api.bfl.ai/v1/flux-2-klein-4b"
|
||||
"https://{{regions}}/{{version}}/{{model_name}}"
|
||||
],
|
||||
"kwargs": {
|
||||
"headers": {
|
||||
@@ -49,7 +60,7 @@
|
||||
"seed": "{{seed}}",
|
||||
"width": "{{width}}",
|
||||
"height": "{{height}}",
|
||||
"prompt_upsampling": "BOOLEAN",
|
||||
"prompt_upsampling": "{{prompt_upsampling}}",
|
||||
"guidance": "{{guidance}}",
|
||||
"steps": "{{steps}}"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
response = requests.post("https://api.bfl.ai/v1/flux-2-klein-4b",
|
||||
response = requests.post("https://{{regions}}/{{version}}/{{model_name}}",
|
||||
headers={
|
||||
"accept": "application/json",
|
||||
"x-key": os.environ.get("BFL_API_KEY"),
|
||||
|
||||
Reference in New Issue
Block a user