diff --git a/clientlibs/utils.js b/clientlibs/utils.js index e19cb8b..500961c 100644 --- a/clientlibs/utils.js +++ b/clientlibs/utils.js @@ -4,60 +4,36 @@ import { DEFAULT_DIMENSIONS_LIST, DEFAULT_MODELS_ARCH_LIST, DEFAULT_CONTROLNET_C RUNWARE_NODE_TYPES, MODEL_TYPES_TERMS, MODEL_LIST_TERMS } from "./types.js"; +const TIMEOUT_RANGE = { min: 5, default: 90, max: 99 }; +const OUTPUT_QUALITY_RANGE = { min: 20, default: 95, max: 99 }; + let openDialog = false; let lastTimeout = false; +let lastOutputFormat = false; +let lastOutputQuality = false; -async function enhancePrompt(userPrompt) { - const resp = await api.fetchApi('/promptEnhance', { - method: "POST", - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - userPrompt: userPrompt - }) - }); - const enhancePromptResults = await resp.json(); - return enhancePromptResults; +async function queryLocalAPI(endpoint, data) { + try { + const resp = await api.fetchApi(`/${endpoint}`, { + method: "POST", + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data) + }); + return await resp.json(); + } catch (err) { + return false; + } } -async function setAPIKey(apiKey) { - const resp = await api.fetchApi('/setAPIKey', { - method: "POST", - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - apiKey: apiKey - }) - }); - const setAPIKeyResults = await resp.json(); - return setAPIKeyResults; -} - -async function setTimeout(maxTimeout) { - const resp = await api.fetchApi('/setMaxTimeout', { - method: "POST", - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - maxTimeout: maxTimeout - }) - }); - const setTimeoutResults = await resp.json(); - return setTimeoutResults; -} - -async function modelSearch(modelQuery = "", modelArch = "all", modelType = "base", modelCat = "checkpoint", condtioning = "") { - const resp = await api.fetchApi('/modelSearch', { - method: "POST", - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - modelQuery: modelQuery, - modelArch: modelArch, - modelType: modelType, - modelCat: modelCat, - condtioning: condtioning - }) - }); - const modelSearchResults = await resp.json(); - return modelSearchResults; -} +const runwareLocalAPI = { + enhancePrompt: (userPrompt) => queryLocalAPI('promptEnhance', { userPrompt }), + setAPIKey: (apiKey) => queryLocalAPI('setAPIKey', { apiKey }), + setTimeout: (maxTimeout) => queryLocalAPI('setMaxTimeout', { maxTimeout }), + setOutputFormat: (outputFormat) => queryLocalAPI('setOutputFormat', { outputFormat }), + setOutputQuality: (outputQuality) => queryLocalAPI('setOutputQuality', { outputQuality }), + modelSearch: (modelQuery = "", modelArch = "all", modelType = "base", modelCat = "checkpoint", condtioning = "") => + queryLocalAPI('modelSearch', { modelQuery, modelArch, modelType, modelCat, condtioning }) +}; function captionNodeHandler(msgEvent) { const captionData = msgEvent.detail; @@ -92,7 +68,7 @@ async function promptEnhanceHandler(e) { notifyUser("Prompt Must Not Exceed 300 Characters!", "error", "Runware Prompt Enhancer"); return; } - const enhanceResults = await enhancePrompt(userPrompt); + const enhanceResults = await runwareLocalAPI.enhancePrompt(userPrompt); if(enhanceResults.success) { e.target.value = enhanceResults.enhancedPrompt; } else { @@ -116,32 +92,61 @@ async function APIKeyHandler(apiManagerNode) { if(apiKey.length < 30) { notifyUser("Invalid API Key Set, Please Try Again!", "error", "Runware API Manager"); return; + } + const setAPIKeyResults = await runwareLocalAPI.setAPIKey(apiKey); + if(setAPIKeyResults.success) { + remNode(apiManagerNode); + notifyUser("API Key Set Successfully!", "success", "Runware API Manager"); } else { - const setAPIKeyResults = await setAPIKey(apiKey); - if(setAPIKeyResults.success) { - remNode(apiManagerNode); - notifyUser("API Key Set Successfully!", "success", "Runware API Manager"); - } else { - notifyUser(setAPIKeyResults.error, "error", "Runware API Manager"); - } + notifyUser(setAPIKeyResults.error, "error", "Runware API Manager"); } }); } else if(widget.name === "Max Timeout") { if(lastTimeout) widget.value = lastTimeout; appendWidgetCB(widget, async function(...args) { - const maxTimeout = args[0]; - if(maxTimeout < 5 || maxTimeout > 99) { - notifyUser("Invalid Timeout Value, Timeout must be between 5 and 99 seconds!", "error", "Runware API Manager"); + const maxTimeout = parseInt(args[0]); + if(isNaN(maxTimeout) || maxTimeout < TIMEOUT_RANGE.min || maxTimeout > TIMEOUT_RANGE.max) { + notifyUser(`Invalid Timeout Value! Must be between ${TIMEOUT_RANGE.min} and ${TIMEOUT_RANGE.max} seconds.`, "error", "Runware API Manager"); + widget.value = lastTimeout || TIMEOUT_RANGE.default; return; + } + const setTimeoutResults = await runwareLocalAPI.setTimeout(maxTimeout); + if(setTimeoutResults.success) { + notifyUser("Timeout Set Successfully!", "success", "Runware API Manager"); + lastTimeout = maxTimeout; } else { - const setTimeoutResults = await setTimeout(maxTimeout); - if(setTimeoutResults.success) { - remNode(apiManagerNode); - notifyUser("Timeout Set Successfully!", "success", "Runware API Manager"); - lastTimeout = maxTimeout; - } else { - notifyUser(setTimeoutResults.error, "error", "Runware API Manager"); - } + widget.value = lastTimeout || TIMEOUT_RANGE.default; + notifyUser(setTimeoutResults.error, "error", "Runware API Manager"); + } + }); + } else if(widget.name === "Image Output Format") { + if(lastOutputFormat) widget.value = lastOutputFormat; + appendWidgetCB(widget, async function(...args) { + const outputFormat = args[0].trim().toUpperCase(); + const setFormatResults = await runwareLocalAPI.setOutputFormat(outputFormat); + if(setFormatResults.success) { + notifyUser("Default Image Output Format Set Successfully!", "success", "Runware API Manager"); + lastOutputFormat = outputFormat; + } else { + notifyUser(setFormatResults.error, "error", "Runware API Manager"); + } + }); + } else if(widget.name === "Image Output Quality") { + if(lastOutputQuality) widget.value = lastOutputQuality; + appendWidgetCB(widget, async function(...args) { + const outputQuality = parseInt(args[0]); + if(isNaN(outputQuality) || outputQuality < OUTPUT_QUALITY_RANGE.min || outputQuality > OUTPUT_QUALITY_RANGE.max) { + notifyUser(`Invalid Quality Value! Must be between ${OUTPUT_QUALITY_RANGE.min} and ${OUTPUT_QUALITY_RANGE.max}.`, "error", "Runware API Manager"); + widget.value = lastOutputQuality || OUTPUT_QUALITY_RANGE.default; + return; + } + const setQualityResults = await runwareLocalAPI.setOutputQuality(outputQuality); + if(setQualityResults.success) { + notifyUser("Default Image Output Quality Set Successfully!", "success", "Runware API Manager"); + lastOutputQuality = outputQuality; + } else { + widget.value = lastOutputQuality || OUTPUT_QUALITY_RANGE.default; + notifyUser(setQualityResults.error, "error", "Runware API Manager"); } }); } @@ -197,6 +202,7 @@ function handleCustomErrors(errObj) { dialog.element.addEventListener('close', () => { openDialog = false; }); + const runwareAPIForm = document.getElementById("runwareAPIKey"); const apiKeyInput = document.getElementById("apiKey"); const getAPIKeyBTN = document.getElementById("getAPIKeyBTN"); @@ -217,7 +223,7 @@ function handleCustomErrors(errObj) { // notifyUser("Invalid API Key Set, Please Try Again!", "error", "Runware API Manager"); // return; } else { - const setAPIKeyResults = await setAPIKey(apiKey); + const setAPIKeyResults = await runwareLocalAPI.setAPIKey(apiKey); if(setAPIKeyResults.success) { dialog.close(); openDialog = false; @@ -242,8 +248,8 @@ function appendWidgetCB(node, newCB) { const oldNodeCB = node.callback; if(typeof oldNodeCB === "function") { node.callback = function(...args) { - newCB?.apply(this, args); oldNodeCB?.apply(this, args); + newCB?.apply(this, args); } } else { node.callback = newCB; @@ -358,11 +364,11 @@ async function searchNodeHandler(searchNode, searchInputWidget) { let modelSearchResults = null; if(isControlNet) { - modelSearchResults = await modelSearch(searchQuery, modelArchValue, "", "controlnet", modelTypeValue); + modelSearchResults = await runwareLocalAPI.modelSearch(searchQuery, modelArchValue, "", "controlnet", modelTypeValue); } else if(isLora || isEmbedding || isVAE) { - modelSearchResults = await modelSearch(searchQuery, modelArchValue, "", modelTypeValue); + modelSearchResults = await runwareLocalAPI.modelSearch(searchQuery, modelArchValue, "", modelTypeValue); } else { - modelSearchResults = await modelSearch(searchQuery, modelArchValue, modelTypeValue); + modelSearchResults = await runwareLocalAPI.modelSearch(searchQuery, modelArchValue, modelTypeValue); } if(modelSearchResults.success) { @@ -524,13 +530,11 @@ async function searchNodeHandler(searchNode, searchInputWidget) { } export { - enhancePrompt, - modelSearch, notifyUser, promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, captionNodeHandler, handleCustomErrors, - APIKeyHandler + APIKeyHandler, }; \ No newline at end of file diff --git a/modules/apiManager.py b/modules/apiManager.py index 42b9942..7038956 100644 --- a/modules/apiManager.py +++ b/modules/apiManager.py @@ -9,14 +9,24 @@ class apiManager: "tooltip": "Define Or Change Your Runware API Key To Start Using Runware Image Inference Services.\n\n(e.g; BcVr1JVQM8FGzrRwb3GsbCq5ww1QwabV)\n\nIf You Don't Have One, You Can Get It From: https://my.runware.ai/keys", }), "Max Timeout": ("INT", { - "tooltip": "Define Or Change Runware API Timeout In Seconds.\n\n(e.g; 90 Seconds).", + "tooltip": "Change Runware API Request Timeout In Seconds.\n\n(e.g; 90 Seconds).", "min": 5, "max": 99, "default": int(rwUtils.getTimeout()), }), + "Image Output Quality": ("INT", { + "tooltip": "Sets the compression quality of the output image. Higher values preserve more quality but increase file size, lower values reduce file size but decrease quality.", + "min": 20, + "max": 99, + "default": int(rwUtils.getOutputQuality()), + }), + "Image Output Format": (["WEBP", "PNG", "JPEG"], { + "tooltip": "Change the Default Image Output Format.", + "default": rwUtils.getOutputFormat(), + }), }, } - DESCRIPTION = "API Managers is a Runware Utility That helps you define or change your API Key or Session Timeout From The ComfyUI Interface Without having to adjust the env file locally." + DESCRIPTION = "API Managers is a Runware Utility That helps you define or change your API Key, Session Timeout, or Image Output Format & Quality From The ComfyUI Interface Without having to adjust the env file locally." CATEGORY = "Runware" RETURN_TYPES = () \ No newline at end of file diff --git a/modules/bgremoval.py b/modules/bgremoval.py index 180f345..490c048 100644 --- a/modules/bgremoval.py +++ b/modules/bgremoval.py @@ -74,6 +74,7 @@ class bgremoval: "returnOnlyMask": returnOnlyMask, "alphaMatting": alphaMatting, "outputFormat": outputFormat, + "outputQuality": rwUtils.OUTPUT_QUALITY, "outputType": "base64Data", } ] diff --git a/modules/bridges/mainRoute.py b/modules/bridges/mainRoute.py index 7d046e8..375af8a 100644 --- a/modules/bridges/mainRoute.py +++ b/modules/bridges/mainRoute.py @@ -34,6 +34,30 @@ async def setMaxTimeout(reqPayload): return web.json_response({'success': False, 'error': str(e)}) return web.json_response({'success': True}) +@routes.post('/setOutputFormat') +async def setOutputFormat(reqPayload): + reqData = await reqPayload.json() + outputFormat = reqData.get('outputFormat', 'WEBP') + if outputFormat not in ['WEBP', 'PNG', 'JPEG']: + return web.json_response({'success': False, 'error': 'Invalid Output Format!'}) + try: + rwUtils.setOutputFormat(outputFormat) + except Exception as e: + return web.json_response({'success': False, 'error': str(e)}) + return web.json_response({'success': True}) + +@routes.post('/setOutputQuality') +async def setOutputQuality(reqPayload): + reqData = await reqPayload.json() + outputQuality = reqData.get('outputQuality', 95) + if not isinstance(outputQuality, int) or outputQuality < 20 or outputQuality > 99: + return web.json_response({'success': False, 'error': 'Invalid Output Quality!'}) + try: + rwUtils.setOutputQuality(outputQuality) + except Exception as e: + return web.json_response({'success': False, 'error': str(e)}) + return web.json_response({'success': True}) + @routes.post('/promptEnhance') async def promptEnhance(reqPayload): reqData = await reqPayload.json() diff --git a/modules/controlNetPreprocessor.py b/modules/controlNetPreprocessor.py index 4b61601..35e9f3d 100644 --- a/modules/controlNetPreprocessor.py +++ b/modules/controlNetPreprocessor.py @@ -96,7 +96,8 @@ class controlNetPreprocessor: "taskUUID": rwUtils.genRandUUID(), "inputImage": rwUtils.convertTensor2IMG(image), "preProcessorType": preProcessorType, - "outputFormat": "WEBP", + "outputFormat": rwUtils.OUTPUT_FORMAT, + "outputQuality": rwUtils.OUTPUT_QUALITY, "outputType": "base64Data", } ] diff --git a/modules/imageInference.py b/modules/imageInference.py index 1e4ad96..2b4464e 100644 --- a/modules/imageInference.py +++ b/modules/imageInference.py @@ -122,7 +122,6 @@ class txt2img: "VAE": ("RUNWAREVAE", { "tooltip": "Connect a Runware VAE Node to help the model generate images that align with the desired structure.", }), - **rwUtils.RUNWARE_OUTPUT_FOMRATS, } } @@ -164,7 +163,6 @@ class txt2img: cfgScale = kwargs.get("cfgScale", 6.5) seed = kwargs.get("seed") batchSize = kwargs.get("batchSize", 1) - outputFormat = kwargs.get("outputFormat", "WEBP") if (maskImage is not None and seedImage is None): raise Exception("Mask Image Requires Seed Image To Be Provided!") @@ -182,8 +180,9 @@ class txt2img: "scheduler": scheduler, "clipSkip": clipSkip, "seed": seed, - "outputFormat": outputFormat, "outputType": "base64Data", + "outputFormat": rwUtils.OUTPUT_FORMAT, + "outputQuality": rwUtils.OUTPUT_QUALITY, "numberResults": batchSize, } ] diff --git a/modules/imageMasking.py b/modules/imageMasking.py index 3d0d627..91d8dab 100644 --- a/modules/imageMasking.py +++ b/modules/imageMasking.py @@ -42,7 +42,6 @@ class imageMasking: "min": 0, "max": 20, }), - **rwUtils.RUNWARE_OUTPUT_FOMRATS, }, } @@ -60,7 +59,6 @@ class imageMasking: maxDetections = kwargs.get("Max Detections") maskPadding = kwargs.get("Mask Padding") maskBlur = kwargs.get("Mask Blur") - outputFormat = kwargs.get("outputFormat", "WEBP") model_mapping = { "face_yolov8n": "runware:35@1", @@ -89,8 +87,9 @@ class imageMasking: "model": detectionModel, "confidence": confidence, "maxDetections": maxDetections, - "outputFormat": outputFormat, "outputType": "base64Data", + "outputFormat": rwUtils.OUTPUT_FORMAT, + "outputQuality": rwUtils.OUTPUT_QUALITY, } ] diff --git a/modules/photoMaker.py b/modules/photoMaker.py index 067d208..144689e 100644 --- a/modules/photoMaker.py +++ b/modules/photoMaker.py @@ -96,7 +96,6 @@ class photoMaker: "min": 1, "max": 10, }), - **rwUtils.RUNWARE_OUTPUT_FOMRATS, }, "optional": { "Image 2": ("IMAGE", { @@ -146,7 +145,6 @@ class photoMaker: clipSkip = kwargs.get("clipSkip", 0) seed = kwargs.get("seed") batchSize = kwargs.get("batchSize", 1) - outputFormat = kwargs.get("outputFormat", "WEBP") imageList = [rwUtils.convertTensor2IMG(image1)] if (image2 is not None): @@ -173,8 +171,9 @@ class photoMaker: "CFGScale": cfgScale, "clipSkip": clipSkip, "numberResults": batchSize, - "outputFormat": outputFormat, "outputType": "base64Data", + "outputFormat": rwUtils.OUTPUT_FORMAT, + "outputQuality": rwUtils.OUTPUT_QUALITY, } ] diff --git a/modules/upscaler.py b/modules/upscaler.py index a332fd5..78f9d9f 100644 --- a/modules/upscaler.py +++ b/modules/upscaler.py @@ -14,7 +14,6 @@ class upscaler: "min": 2, "max": 4, }), - **rwUtils.RUNWARE_OUTPUT_FOMRATS, }, } @@ -26,7 +25,6 @@ class upscaler: def upscale(self, **kwargs): image = kwargs.get("Image") upscaleFactor = kwargs.get("upscaleFactor", 2) - outputFormat = kwargs.get("outputFormat", "WEBP") genConfig = [ { @@ -34,7 +32,8 @@ class upscaler: "taskUUID": rwUtils.genRandUUID(), "inputImage": rwUtils.convertTensor2IMG(image), "upscaleFactor": upscaleFactor, - "outputFormat": outputFormat, + "outputFormat": rwUtils.OUTPUT_FORMAT, + "outputQuality": rwUtils.OUTPUT_QUALITY, "outputType": "base64Data", } ] diff --git a/modules/utils/runwareUtils.py b/modules/utils/runwareUtils.py index 298636f..85e4292 100644 --- a/modules/utils/runwareUtils.py +++ b/modules/utils/runwareUtils.py @@ -15,13 +15,6 @@ import io load_dotenv() -RUNWARE_OUTPUT_FOMRATS = { - "outputFormat": (["WEBP", "PNG", "JPG"], { - "default": "WEBP", - "tooltip": "Choose the output image format." - }) -} - RUNWARE_REMBG_OUTPUT_FORMATS = { "outputFormat": (["WEBP", "PNG"], { "default": "WEBP", @@ -50,8 +43,28 @@ def getTimeout(): os.environ["RUNWARE_TIMEOUT"] = str(timeout) return timeout +def getOutputQuality(): + output_quality = os.getenv("RUNWARE_OUTPUT_QUALITY") + if output_quality and isinstance(output_quality, str) and output_quality.isdigit(): + return int(output_quality) + else: + output_quality = 95 + os.environ["RUNWARE_OUTPUT_QUALITY"] = str(output_quality) + return output_quality + +def getOutputFormat(): + output_format = os.getenv("RUNWARE_OUTPUT_FORMAT") + if output_format and isinstance(output_format, str): + return output_format + else: + output_format = "WEBP" + os.environ["RUNWARE_OUTPUT_FORMAT"] = output_format + return output_format + SESSION_TIMEOUT = getTimeout() RUNWARE_API_KEY = getAPIKey() +OUTPUT_FORMAT = getOutputFormat() +OUTPUT_QUALITY = getOutputQuality() def setEnvKey(keyName, keyValue): comfyNodeRoot = Path(__file__).parent.parent.parent @@ -90,6 +103,22 @@ def setTimeout(timeout: int): os.environ["RUNWARE_TIMEOUT"] = str(timeout) return True +def setOutputFormat(format: str): + envSetRes = setEnvKey("RUNWARE_OUTPUT_FORMAT", format) + if envSetRes: + global OUTPUT_FORMAT + OUTPUT_FORMAT = format + os.environ["RUNWARE_OUTPUT_FORMAT"] = format + return True + +def setOutputQuality(quality: int): + envSetRes = setEnvKey("RUNWARE_OUTPUT_QUALITY", str(quality)) + if envSetRes: + global OUTPUT_QUALITY + OUTPUT_QUALITY = quality + os.environ["RUNWARE_OUTPUT_QUALITY"] = str(quality) + return True + def genRandSeed(minSeed = 1000, maxSeed = 9223372036854776000): return random.randint(minSeed, maxSeed)