From 00392a4c836cb578adb6743f0712e56cdb3082ca Mon Sep 17 00:00:00 2001 From: "DESKTOP-TVBJISQ\\Primere" Date: Tue, 10 Mar 2026 08:35:51 +0100 Subject: [PATCH] V 2.0.0 - Universal api #74 - user files --- Nodes/Dashboard.py | 67 +++++++++++++++++++ Nodes/Uniapi.py | 4 +- __init__.py | 2 + components/API/api_helper.py | 3 + components/primereserver.py | 8 ++- ..._schemas.json => api_schemas.example.json} | 0 front_end/frontend_helper.js | 2 +- front_end/primere_uniapi.js | 40 +++++++++-- 8 files changed, 118 insertions(+), 8 deletions(-) rename front_end/{api_schemas.json => api_schemas.example.json} (100%) diff --git a/Nodes/Dashboard.py b/Nodes/Dashboard.py index 126fe3a..30d7ae1 100644 --- a/Nodes/Dashboard.py +++ b/Nodes/Dashboard.py @@ -645,6 +645,73 @@ class PrimereModelConceptSelector: zimage_model, zimage_clip, zimage_vae ) +class PrimereControlledSamplersSteps: + CATEGORY = TREE_DASHBOARD + RETURN_TYPES = ("STRING", comfy.samplers.KSampler.SAMPLERS, comfy.samplers.KSampler.SCHEDULERS, "INT", "FLOAT") + RETURN_NAMES = ("MODEL_CONCEPT", "SAMPLER_NAME", "SCHEDULER_NAME", "STEPS", "CFG") + FUNCTION = "get_controlledsampler_step" + + kolors_schedulers = ["EulerDiscreteScheduler", "EulerAncestralDiscreteScheduler", "DPMSolverMultistepScheduler", "DPMSolverMultistepScheduler_SDE_karras", "UniPCMultistepScheduler", "DEISMultistepScheduler"] + sana_schedulers = ['flow_dpm-solver'] + + UNETLIST = PrimereModelConceptSelector.UNETLIST + DIFFUSIONLIST = PrimereModelConceptSelector.DIFFUSIONLIST + TEXT_ENCODERS = PrimereModelConceptSelector.TEXT_ENCODERS + GGUFLIST = PrimereModelConceptSelector.GGUFLIST + VAELIST = PrimereModelConceptSelector.VAELIST + CLIPLIST = PrimereModelConceptSelector.CLIPLIST + MODELLIST = PrimereModelConceptSelector.MODELLIST + TEXT_ENCODERS_PATHS = PrimereModelConceptSelector.TEXT_ENCODERS_PATHS + + CONCEPT_LIST = PrimereModelConceptSelector.CONCEPT_LIST + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model_concept": ("STRING", {"default": None, "forceInput": True}), + "concepts": (["Auto"] + cls.CONCEPT_LIST,), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler_name": (cls.sana_schedulers + cls.kolors_schedulers + comfy.samplers.KSampler.SCHEDULERS,), + "steps": ("INT", {"default": 12, "min": 1, "max": 1000, "step": 1}), + "cfg": ("FLOAT", {"default": 7, "min": 0.1, "max": 100, "step": 0.01}), + "vae": (["None"] + cls.VAELIST,), + "encoder_1": (["None"] + cls.TEXT_ENCODERS + cls.CLIPLIST + cls.TEXT_ENCODERS_PATHS,), + "encoder_2": (["None"] + cls.TEXT_ENCODERS + cls.CLIPLIST + cls.TEXT_ENCODERS_PATHS,), + "encoder_3": (["None"] + cls.TEXT_ENCODERS + cls.CLIPLIST + cls.TEXT_ENCODERS_PATHS,), + "sampler": (["None"] + ["custom_advanced", "ksampler"], {"default": "ksampler"}), + "guidance": ('FLOAT', {"default": 3.5, "min": 0.0, "max": 100.0, "step": 0.1}), + "weight_dtype": (["None"] + ["Auto", "default", "fp16", "bf16", "fp32", "fp8_e4m3fn", "fp8_e5m2"], {"default": "default"}), + "precision": (["None"] + ['fp32', 'fp16', 'quant8', 'quant4'], {"default": "fp16"}), + "use_speed_lora": ("BOOLEAN", {"default": False, "label_on": "Seed lora ON", "label_off": "Seed lora OFF"}), + "speed_lora": (["None"],), + "speed_lora_version": ([1.0, 1.1, 2.0], {"default": 2.0}), + "speed_lora_precision": ("BOOLEAN", {"default": True, "label_on": "FP32", "label_off": "BF16"}), + "speed_lora_step": ([4, 6, 8, 10, 12, 16], {"default": 8}), + "speed_lora_strength": ("FLOAT", {"default": 1.00, "min": -20.00, "max": 20.00, "step": 0.01}), + "use_srpo_lora": ("BOOLEAN", {"default": False, "label_on": "Use SRPO Lora", "label_off": "Ignore SRPO Lora"}), + "use_srpo_svdq_lora": ("BOOLEAN", {"default": False, "label_on": "Use SRPO-NUNCHAKU Lora", "label_off": "Ignore SRPO-NUNCHAKU Lora"}), + "srpo_lora_type": (["R&Q", "RockerBOO", "oficial", "adaptive"], {"default": "oficial"}), + "srpo_lora_rank": ([8, 16, 32, 64, 128, 256], {"default": 8}), + "srpo_lora_strength": ("FLOAT", {"default": 1, "min": -20.000, "max": 20.000, "step": 0.001}), + "use_nunchaku_lora": ("BOOLEAN", {"default": False, "label_on": "Use nunchaku Lora", "label_off": "Ignore nunchaku Lora"}), + "nunchaku_lora_type": (["kontext_deblur", "kontext_face_detailer", "anything_extracted"], {"default": "anything_extracted"}), + "nunchaku_lora_rank": ([64, 256], {"default": 64}), + "nunchaku_lora_strength": ("FLOAT", {"default": 1, "min": -20.000, "max": 20.000, "step": 0.001}), + "pixart_refiner_model": (["None"] + cls.MODELLIST,), + "refiner_sampler": (comfy.samplers.KSampler.SAMPLERS, {"default": "dpmpp_2m"}), + "refiner_scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "normal"}), + "refiner_cfg": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 100, "step": 0.01}), + "refiner_steps": ("INT", {"default": 22, "min": 10, "max": 30, "step": 1}), + "refiner_start": ("INT", {"default": 12, "min": 1, "max": 1000, "step": 1}), + "refiner_denoise": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}), + "refiner_ignore_prompt": ("BOOLEAN", {"default": False, "label_on": "Send prompt to refiner", "label_off": "Ignore prompt"}), + } + } + + def get_controlledsampler_step(self, model_concept, sampler_name, scheduler_name, steps=12, cfg=7, **kwargs): + return model_concept, sampler_name, scheduler_name, steps, round(cfg, 2) + class PrimereConceptDataTuple: RETURN_TYPES = ("TUPLE",) RETURN_NAMES = ("CONCEPT_DATA",) diff --git a/Nodes/Uniapi.py b/Nodes/Uniapi.py index fd3d2fe..0d57840 100644 --- a/Nodes/Uniapi.py +++ b/Nodes/Uniapi.py @@ -33,7 +33,9 @@ class PrimereApiProcessor: FUNCTION = "process_uniapi" API_RESULT = api_helper.get_api_config("apiconfig.json") - API_SCHEMAS_RAW = utility.json2tuple(os.path.join(PRIMERE_ROOT, 'front_end', 'api_schemas.json')) + _schema_file = os.path.join(PRIMERE_ROOT, 'front_end', 'api_schemas.json') + _schema_example = os.path.join(PRIMERE_ROOT, 'front_end', 'api_schemas.example.json') + API_SCHEMAS_RAW = utility.json2tuple(_schema_file if Path(_schema_file).is_file() else _schema_example) API_SCHEMA_REGISTRY = api_schema_registry.normalize_registry(API_SCHEMAS_RAW) @classmethod diff --git a/__init__.py b/__init__.py index 37f5495..ff3eb9a 100644 --- a/__init__.py +++ b/__init__.py @@ -70,6 +70,7 @@ for subdirs in valid_FElist: NODE_CLASS_MAPPINGS = { "PrimereSamplersSteps": Dashboard.PrimereSamplersSteps, + "PrimereControlledSamplersSteps": Dashboard.PrimereControlledSamplersSteps, "PrimereVAE": Dashboard.PrimereVAE, "PrimereCKPT": Dashboard.PrimereCKPT, "PrimereVAELoader": Dashboard.PrimereVAELoader, @@ -148,6 +149,7 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "PrimereSamplersSteps": "Primere Samplers & Steps & Cfg", + "PrimereControlledSamplersSteps": "Primere Controlled Sampler Setting", "PrimereVAE": "Primere VAE Selector", "PrimereCKPT": "Primere CKPT Selector", "PrimereVAELoader": "Primere VAE Loader", diff --git a/components/API/api_helper.py b/components/API/api_helper.py index 2f8bdd0..7c561c9 100644 --- a/components/API/api_helper.py +++ b/components/API/api_helper.py @@ -7,6 +7,9 @@ def get_api_config(name: str) -> dict: fp = os.path.join(path, name) config_json = utility.json2tuple(fp) + if not config_json: + return {} + for k, v in config_json.items(): match k: case "OpenAI": diff --git a/components/primereserver.py b/components/primereserver.py index cf542da..c6c51e0 100644 --- a/components/primereserver.py +++ b/components/primereserver.py @@ -521,4 +521,10 @@ async def primere_prompt_saver(request): PromptServer.instance.send_sync("PromptDataSaveResponse", False) else: PromptServer.instance.send_sync("PromptDataSaveResponse", False) - return web.json_response({}) \ No newline at end of file + return web.json_response({}) + +routes17 = PromptServer.instance.routes +@routes17.get('/primere_apiconfig_check') +async def primere_apiconfig_check(request): + config_path = os.path.join(PRIMERE_ROOT, 'json', 'apiconfig.json') + return web.json_response({"exists": os.path.isfile(config_path)}) \ No newline at end of file diff --git a/front_end/api_schemas.json b/front_end/api_schemas.example.json similarity index 100% rename from front_end/api_schemas.json rename to front_end/api_schemas.example.json diff --git a/front_end/frontend_helper.js b/front_end/frontend_helper.js index aac5ded..74501df 100644 --- a/front_end/frontend_helper.js +++ b/front_end/frontend_helper.js @@ -82,7 +82,7 @@ export function showToast(status, message) { const textEl = document.createElement("span"); textEl.textContent = message; - Object.assign(textEl.style, { flex: "1", lineHeight: "1.5", wordBreak: "break-all" }); + Object.assign(textEl.style, { flex: "1", lineHeight: "1.5", wordBreak: "normal", overflowWrap: "break-word" }); const closeBtn = document.createElement("button"); closeBtn.textContent = "✕"; diff --git a/front_end/primere_uniapi.js b/front_end/primere_uniapi.js index 17bbb8a..1aff7e5 100644 --- a/front_end/primere_uniapi.js +++ b/front_end/primere_uniapi.js @@ -3,9 +3,11 @@ import { applyPrimereButtonStyle, showToast } from "./frontend_helper.js"; const TARGET_NODE_NAME = "PrimereApiProcessor"; const SCHEMA_URL = new URL("/extensions/ComfyUI_Primere_Nodes/api_schemas.json", import.meta.url).href; +const SCHEMA_EXAMPLE_URL = new URL("/extensions/ComfyUI_Primere_Nodes/api_schemas.example.json", import.meta.url).href; let schemaCache = null; let schemaPromise = null; +let apiconfigChecked = false; async function loadSchemas() { if (schemaCache) { @@ -16,7 +18,12 @@ async function loadSchemas() { schemaPromise = fetch(SCHEMA_URL) .then((response) => { if (!response.ok) { - throw new Error(`Cannot load schema file (${response.status})`); + return fetch(SCHEMA_EXAMPLE_URL).then((fallback) => { + if (!fallback.ok) { + throw new Error(`Cannot load schema file (${fallback.status})`); + } + return fallback.json(); + }); } return response.json(); }) @@ -25,7 +32,7 @@ async function loadSchemas() { return schemaCache; }) .catch((error) => { - console.error("[Primere UniApi] Failed to load json/api_schemas.json", error); + console.error("[Primere UniApi] Failed to load api_schemas.json and api_schemas.example.json", error); return {}; }); } @@ -301,6 +308,15 @@ async function initializeUniApiNode(node) { const refreshBtn = node.addWidget("button", "↺ Reload API Schema", null, async () => { schemaCache = null; schemaPromise = null; + let apiconfigMissing = false; + try { + const r = await fetch("/primere_apiconfig_check"); + const data = r.ok ? await r.json() : null; + if (data && !data.exists) { + apiconfigMissing = true; + showToast("error", "API config file (json/apiconfig.json) not found. Create it with your API keys before using this node. See the manual for details."); + } + } catch (_) {} try { const freshRegistry = await loadSchemas(); if (!freshRegistry || Object.keys(freshRegistry).length === 0) { @@ -309,9 +325,11 @@ async function initializeUniApiNode(node) { } updateServiceWidget(node, freshRegistry); updateParameterWidgets(node, freshRegistry); - const providerCount = Object.keys(freshRegistry).length; - const serviceCount = Object.values(freshRegistry).reduce((sum, p) => sum + Object.keys(p).length, 0); - showToast("success", `API Schema reloaded successfully. ${providerCount} provider(s), ${serviceCount} service(s) loaded.`); + if (!apiconfigMissing) { + const providerCount = Object.keys(freshRegistry).length; + const serviceCount = Object.values(freshRegistry).reduce((sum, p) => sum + Object.keys(p).length, 0); + showToast("success", `API Schema reloaded successfully. ${providerCount} provider(s), ${serviceCount} service(s) loaded.`); + } } catch (error) { showToast("error", `API Schema reload failed. ${error.message}`); } @@ -350,6 +368,18 @@ async function initializeUniApiNode(node) { }; } + if (!apiconfigChecked) { + apiconfigChecked = true; + fetch("/primere_apiconfig_check") + .then((r) => r.ok ? r.json() : null) + .then((data) => { + if (data && !data.exists) { + showToast("error", "API config file (json/apiconfig.json) not found. Create it with your API keys before using this node. See the manual for details."); + } + }) + .catch(() => {}); + } + const schemaRegistry = await loadSchemas(); updateServiceWidget(node, schemaRegistry); updateParameterWidgets(node, schemaRegistry);