V2.0.0 - Mistral

This commit is contained in:
DESKTOP-TVBJISQ\Primere
2026-05-13 13:21:28 +02:00
parent 929733149d
commit 162c327bce
8 changed files with 196 additions and 18 deletions
+9 -1
View File
@@ -7,6 +7,7 @@ from ..components import utility
from ..components.API import api_helper
import folder_paths
import re
import random
import argparse
import json
@@ -41,7 +42,7 @@ class PrimereApiProcessor:
SECTION_TITLES = [
{"before": "processor", "name": "primere_api_proc", "title": "🧭 API Setup", "color": "#1B263B", "text_color": "#EAF1F8", "label": "Setup API processor, select related provider and service."},
{"before": "auto_save_result", "name": "primere_save_api_result", "title": "💾 API result save", "color": "#1B263B", "text_color": "#EAF1F8", "label": "Save API results. Define main path, add subdirectory structure, add filename prefixes. Save related data to .txt or .json file."},
{"after": "save_data_to_txt", "name": "primere_api_body", "title": "⚙ API body parameters", "color": "#1B263B", "text_color": "#EAF1F8", "label": "Set API body custom parameters."},
{"after": "save_response_to_json", "name": "primere_api_body", "title": "⚙ API body parameters", "color": "#1B263B", "text_color": "#EAF1F8", "label": "Set API body custom parameters."},
]
@classmethod
@@ -69,6 +70,7 @@ class PrimereApiProcessor:
"image_quality": ("INT", {"default": 95, "min": 1, "max": 100, "step": 1}),
"save_data_to_json": ("BOOLEAN", {"default": False}),
"save_data_to_txt": ("BOOLEAN", {"default": False}),
"save_response_to_json": ("BOOLEAN", {"default": False}),
}
cls.optional_inputs = {
@@ -378,6 +380,7 @@ class PrimereApiProcessor:
image_quality = kwargs.get('image_quality', 95)
save_data_to_json = kwargs.get('save_data_to_json', False)
save_data_to_txt = kwargs.get('save_data_to_txt', False)
save_response_to_json = kwargs.get('save_response_to_json', False)
add_model_to_path = kwargs.get('add_model_to_path', False)
model_subdir = None
@@ -422,6 +425,11 @@ class PrimereApiProcessor:
}
file_output.save_metadata(save_data, json_file, txt_file, save_data_to_json, save_data_to_txt, used_values_output)
if save_response_to_json:
response_json_file = os.path.splitext(saved_path)[0] + '.response.json'
with open(response_json_file, 'w', encoding='utf-8') as response_file:
json.dump(api_result_debug, response_file, ensure_ascii=False, indent=4, default=str)
PromptServer.instance.send_sync("primere.save_result", {
"node_id": unique_id,
"status": "success",
+7
View File
@@ -26,6 +26,10 @@ def get_api_config(name: str) -> dict:
config_json['FAL']['APIKEY'] = os.environ.get("FAL_API_KEY") or config_json['FAL']['APIKEY']
case "Elevenlabs":
config_json['Elevenlabs']['APIKEY'] = os.environ.get("ELEVENLABS_API_KEY") or config_json['Elevenlabs']['APIKEY']
case "Mistral":
config_json['Mistral']['APIKEY'] = os.environ.get("MISTRAL_API_KEY") or config_json['Mistral']['APIKEY']
case "Opencode":
config_json['Opencode']['APIKEY'] = os.environ.get("OPENCODE_API_KEY") or config_json['Opencode']['APIKEY']
return config_json
@@ -54,6 +58,9 @@ def create_api_client(api_provider, config_json):
from elevenlabs.client import ElevenLabs
load_dotenv()
APIClient = ElevenLabs(api_key=config_json['Elevenlabs']['APIKEY'])
case "Mistral":
from mistralai.client import Mistral
APIClient = Mistral(api_key=config_json['Mistral']['APIKEY'])
case _:
return (None, None)
+16
View File
@@ -19,6 +19,7 @@ import numpy as np
import torch
import comfy.utils
import types
import ast
from . import request_exceptions
@@ -623,6 +624,20 @@ def apply_response_handler(schema: dict[str, Any] | None, api_result: Any, provi
return handler(api_result, **kwargs)
def sanitize_debug_value(value: Any) -> Any:
if isinstance(value, str):
text_value = value.strip()
if text_value:
try:
parsed_json = json.loads(text_value)
return sanitize_debug_value(parsed_json)
except Exception:
normalized = re.sub(r"\bUnset\s*\(\s*\)", "None", text_value)
try:
parsed_literal = ast.literal_eval(normalized)
return sanitize_debug_value(parsed_literal)
except Exception:
pass
if isinstance(value, torch.Tensor):
return f"[torch.Tensor omitted: shape={tuple(value.shape)}, dtype={value.dtype}]"
@@ -689,6 +704,7 @@ def sanitize_debug_value(value: Any) -> Any:
def sanitize_api_debug_payload(value: Any) -> Any:
return redact_reference_images(sanitize_debug_value(value))
def apply_parameter_constraints(selected_parameters: dict[str, Any], schema: dict[str, Any] | None) -> dict[str, Any]:
if not isinstance(schema, dict):
return selected_parameters
+77
View File
@@ -0,0 +1,77 @@
from __future__ import annotations
import ast
import dataclasses
import json
import re
from typing import Any
def _normalize_input(value: Any) -> Any:
if isinstance(value, str):
text_value = value.strip()
if not text_value:
return value
try:
return json.loads(text_value)
except Exception:
normalized = re.sub(r"\bUnset\s*\(\s*\)", "None", text_value)
try:
return ast.literal_eval(normalized)
except Exception:
return value
return value
def _to_builtin(value: Any) -> Any:
value = _normalize_input(value)
if isinstance(value, dict):
return {str(k): _to_builtin(v) for k, v in value.items()}
if isinstance(value, list):
return [_to_builtin(item) for item in value]
if isinstance(value, tuple):
return [_to_builtin(item) for item in value]
if dataclasses.is_dataclass(value):
return _to_builtin(dataclasses.asdict(value))
if hasattr(value, "__dict__") and not isinstance(value, (str, int, float, bool, type(None))):
return {str(k): _to_builtin(v) for k, v in vars(value).items() if not str(k).startswith("_")}
return value
def _extract_content(payload: Any) -> str | None:
data = _to_builtin(payload)
if not isinstance(data, dict):
return None
choices = data.get("choices")
if not isinstance(choices, list) or len(choices) == 0:
return None
first_choice = choices[0] if isinstance(choices[0], dict) else None
if not isinstance(first_choice, dict):
return None
message = first_choice.get("message")
if not isinstance(message, dict):
return None
content = message.get("content")
if isinstance(content, str) and content.strip():
return content
return None
def handle_response(api_result, schema=None, loaded_client=None, response_url=None, client=None, sdk_context=None):
payload = _to_builtin(api_result)
content = _extract_content(payload)
if content is not None:
return ["text_result", content.encode("utf-8")]
fallback = {
"error": "Mistral content not found in response. Saved full response instead.",
"response": payload,
}
fallback_text = json.dumps(fallback, ensure_ascii=False, indent=2, default=str)
return ["text_result", fallback_text.encode("utf-8")]
-1
View File
@@ -1,5 +1,4 @@
from __future__ import annotations
import base64
from . import response_helper
+2 -2
View File
@@ -122,8 +122,8 @@ def resolve_output_file(output_path_input, base_dir, subdirs, prefix, delimiter,
)
output_file = os.path.abspath(os.path.join(resolved, filename))
json_file = os.path.splitext(output_file)[0] + '.json'
txt_file = os.path.splitext(output_file)[0] + '.txt'
json_file = os.path.splitext(output_file)[0] + '_meta.json'
txt_file = os.path.splitext(output_file)[0] + '_meta.txt'
return output_file, json_file, txt_file
+74 -3
View File
@@ -18,9 +18,34 @@
"fake_reference_image_value_1",
"fake_reference_image_value_2"
],
"aspect_ratio": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"],
"duration": ["auto", 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
"resolution": ["480p", "720p"],
"aspect_ratio": [
"auto",
"21:9",
"16:9",
"4:3",
"1:1",
"3:4",
"9:16"
],
"duration": [
"auto",
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15
],
"resolution": [
"480p",
"720p"
],
"video_urls": [
"fake_video_urls_value_1",
"fake_video_urls_value_2"
@@ -53,5 +78,51 @@
}
}
}
},
"Mistral": {
"Chat": {
"provider": "Mistral",
"service": "Chat",
"response_handler": "Mistral_Chat.py",
"reference_images_handler": "Mistral.py",
"import_modules": [
"import your_provider_sdk",
"from your_provider_sdk import types"
],
"possible_parameters": {
"model": [
"example-model-1",
"example-model-2"
],
"response": [
"fake_response_value_1",
"fake_response_value_2"
],
"role": [
"fake_role_value_1",
"fake_role_value_2"
]
},
"request": {
"method": "SDK",
"endpoint": "mistral.chat.complete",
"sdk_call": {
"args": [],
"kwargs": {
"model": "{{model}}",
"messages": [
{
"role": "{{role}}",
"content": "{{prompt}}"
}
],
"stream": false,
"response_format": {
"type": "{{response}}"
}
}
}
}
}
}
}
+11 -11
View File
@@ -1,13 +1,13 @@
result = fal_client.subscribe("bytedance/seedance-2.0/fast/reference-to-video",
arguments={
"prompt": prompt,
"image_urls": reference_image,
"video_urls": video_urls,
"audio_urls": audio_urls,
"resolution": resolution,
"seed": seed,
"duration": "auto",
"aspect_ratio": "auto",
"with_logs": with_logs
res = mistral.chat.complete(
model=model,
messages=[
{
"role": role,
"content": prompt
},
],
stream=False,
response_format={
"type": response,
}
)