From ef2f59632555223fc78058c6bc45fd6a09e79a88 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Mon, 11 May 2026 14:07:40 +0530 Subject: [PATCH] Added fix for numResults --- modules/audioInference.py | 118 +++++++++++++----- ...Runware_Audio_Inference_Inworld_TTS_2.json | 1 + 2 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 workflows/Runware_Audio_Inference_Inworld_TTS_2.json diff --git a/modules/audioInference.py b/modules/audioInference.py index 666d981..cac23be 100644 --- a/modules/audioInference.py +++ b/modules/audioInference.py @@ -5,6 +5,7 @@ import numpy as np import librosa import tempfile import os +from urllib.parse import urlparse import comfy.model_management from .utils import runwareUtils as rwUtils @@ -22,10 +23,6 @@ class RunwareAudioInference: "model": ("RUNWAREAUDIOMODEL", { "tooltip": "AI model to use for audio generation" }), - "usePositivePrompt": ("BOOLEAN", { - "default": True, - "tooltip": "Enable/disable positivePrompt parameter in API request" - }), "positivePrompt": ("STRING", { "multiline": True, "default": "classical piano piece, gentle and melodic", @@ -74,10 +71,6 @@ class RunwareAudioInference: "step": 1, "tooltip": "Number of audio channels (1=mono, 2=stereo)" }), - "outputType": (["URL", "dataURI", "base64Data"], { - "default": "URL", - "tooltip": "Output type for the generated audio" - }), "outputFormat": (["MP3", "MP4", "WAV", "FLAC", "OGG"], { "default": "MP3", "tooltip": "Format of the output audio" @@ -219,13 +212,16 @@ class RunwareAudioInference: status = audioData["status"] if status == "success": - audioUrl = self._extractAudioUrl(pollResult) - hasAudio = audioUrl is not None + audioUrls = self._extractAudioUrls(pollResult) + hasAudio = len(audioUrls) > 0 hasVideo = bool(audioData.get("videoURL") or audioData.get("videoBase64Data", False)) if hasAudio: - audioObj = self._downloadAndProcessAudio(audioUrl, params["sampleRate"]) - print(f"[DEBUG] Audio URL found, returning audio. Audio URL: {audioUrl}") + audioObjects = [] + for audioUrl in audioUrls: + audioObjects.append(self._downloadAndProcessAudio(audioUrl, params["sampleRate"], params["outputFormat"])) + audioObj = self._mergeAudioObjects(audioObjects, params["sampleRate"]) + print(f"[DEBUG] Audio URL(s) found, returning batched audio. Count: {len(audioUrls)}") # Return empty video object when only audio is present (prevents errors in downstream nodes) emptyVideoObj = rwUtils.VideoObject("", width=0, height=0) return (audioObj, emptyVideoObj) @@ -260,7 +256,6 @@ class RunwareAudioInference: """Extract and validate parameters from kwargs""" return { "positivePrompt": kwargs.get("positivePrompt", ""), - "usePositivePrompt": kwargs.get("usePositivePrompt", True), "model": kwargs.get("model", ""), "duration": kwargs.get("duration", 30), "useDuration": kwargs.get("useDuration", True), @@ -270,7 +265,6 @@ class RunwareAudioInference: "useBitrate": kwargs.get("useBitrate", False), "channels": kwargs.get("channels", 2), "useChannels": kwargs.get("useChannels", False), - "outputType": kwargs.get("outputType", "URL"), "outputFormat": kwargs.get("outputFormat", "MP3"), "negativePrompt": kwargs.get("negativePrompt", ""), "numberResults": kwargs.get("numberResults", 1), @@ -310,7 +304,7 @@ class RunwareAudioInference: "taskType": "audioInference", "taskUUID": taskUuid, "model": params["model"], - "outputType": params["outputType"], + "outputType": "URL", "outputFormat": params["outputFormat"], "deliveryMethod": "async", "includeCost": True, @@ -338,8 +332,8 @@ class RunwareAudioInference: params["useDuration"] = False print(f"[DEBUG] Disabled duration because sections are provided") - # Add prompts conditionally - if params["usePositivePrompt"]: + # Add positivePrompt only when non-empty + if len(params["positivePrompt"]) > 0: genConfig[0]["positivePrompt"] = params["positivePrompt"] print(f"[DEBUG] Added positivePrompt: '{params['positivePrompt']}'") else: @@ -447,26 +441,68 @@ class RunwareAudioInference: if not hasAudio and not hasVideo: raise Exception("No audio or video data received from API") - def _extractAudioUrl(self, genResult): - """Extract audio URL from API response. Returns None if no audio URL is present (e.g., when only video is returned).""" - audioData = genResult["data"][0] - - audioUrl = audioData.get("audioURL", "") - if not audioUrl: - audioUrl = audioData.get("audioDataURI", "") - if not audioUrl: - audioUrl = audioData.get("audioBase64Data", "") - + def _extractAudioUrls(self, genResult): + """Extract all available audio URLs from API response.""" + audioUrls = [] + for item in genResult.get("data", []): + audioUrl = item.get("audioURL", "") or item.get("audioDataURI", "") or item.get("audioBase64Data", "") + if audioUrl: + audioUrls.append(audioUrl) + return audioUrls - return audioUrl if audioUrl else None + def _mergeAudioObjects(self, audioObjects, targetSampleRate): + """Merge multiple audio objects into a single batched audio object.""" + if not audioObjects: + return { + "waveform": torch.zeros((1, 1, 1)), + "sample_rate": targetSampleRate, + "format": "wav" + } - def _downloadAndProcessAudio(self, audioUrl, targetSampleRate): + waveforms = [] + maxChannels = 0 + maxSamples = 0 + + for audioObj in audioObjects: + waveform = audioObj["waveform"] + if waveform.dim() == 2: + waveform = waveform.unsqueeze(0) + waveforms.append(waveform) + maxChannels = max(maxChannels, waveform.shape[1]) + maxSamples = max(maxSamples, waveform.shape[2]) + + paddedWaveforms = [] + for waveform in waveforms: + batchSize, channels, samples = waveform.shape + + if channels < maxChannels: + channelPadding = torch.zeros( + (batchSize, maxChannels - channels, samples), + dtype=waveform.dtype, + device=waveform.device + ) + waveform = torch.cat([waveform, channelPadding], dim=1) + + if samples < maxSamples: + waveform = torch.nn.functional.pad(waveform, (0, maxSamples - samples)) + + paddedWaveforms.append(waveform) + + outputFormat = audioObjects[0].get("format", "wav") + return { + "waveform": torch.cat(paddedWaveforms, dim=0), + "sample_rate": targetSampleRate, + "format": outputFormat + } + + def _downloadAndProcessAudio(self, audioUrl, targetSampleRate, outputFormat): """Download audio file and process it for ComfyUI""" try: response = requests.get(audioUrl, timeout=30) response.raise_for_status() - - with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as tempFile: + + tempSuffix = self._inferAudioSuffix(audioUrl, outputFormat) + with tempfile.NamedTemporaryFile(suffix=tempSuffix, delete=False) as tempFile: tempFile.write(response.content) tempFilePath = tempFile.name @@ -477,7 +513,8 @@ class RunwareAudioInference: return { "waveform": waveformTensor, - "sample_rate": targetSampleRate + "sample_rate": targetSampleRate, + "format": tempSuffix.lstrip(".") } finally: if os.path.exists(tempFilePath): @@ -485,6 +522,23 @@ class RunwareAudioInference: except Exception as e: raise Exception(f"Failed to download audio: {e}") + def _inferAudioSuffix(self, audioUrl, outputFormat): + """Infer temporary audio file extension from URL or requested format.""" + parsedPath = urlparse(audioUrl).path if audioUrl else "" + ext = os.path.splitext(parsedPath)[1].lower() + allowed = {".mp3", ".mp4", ".wav", ".flac", ".ogg"} + if ext in allowed: + return ext + + formatMap = { + "MP3": ".mp3", + "MP4": ".mp4", + "WAV": ".wav", + "FLAC": ".flac", + "OGG": ".ogg", + } + return formatMap.get(str(outputFormat).upper(), ".wav") + def _loadAudioFile(self, filePath): """Load audio file using librosa""" waveformNp, originalSampleRate = librosa.load(filePath, sr=None, mono=False) diff --git a/workflows/Runware_Audio_Inference_Inworld_TTS_2.json b/workflows/Runware_Audio_Inference_Inworld_TTS_2.json new file mode 100644 index 0000000..2246d78 --- /dev/null +++ b/workflows/Runware_Audio_Inference_Inworld_TTS_2.json @@ -0,0 +1 @@ +{"id":"63c5d240-4ae4-4bbc-9ac5-6987dc62e16d","revision":0,"last_node_id":4,"last_link_id":3,"nodes":[{"id":3,"type":"SaveAudio","pos":[1014.7385922796449,-134.91065284200786],"size":[270,112],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","link":1},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.19.3"},"widgets_values":["audio/ComfyUI"]},{"id":2,"type":"Runware Audio Model Search","pos":[98.84826209096593,-173.39090048351736],"size":[270,130],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"Model Search","name":"Model Search","type":"STRING","widget":{"name":"Model Search"},"link":null},{"localized_name":"Model Provider","name":"Model Provider","type":"COMBO","widget":{"name":"Model Provider"},"link":null},{"localized_name":"AudioList","name":"AudioList","type":"COMBO","widget":{"name":"AudioList"},"link":null},{"localized_name":"Use Search Value","name":"Use Search Value","type":"BOOLEAN","widget":{"name":"Use Search Value"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"RUNWAREAUDIOMODEL","links":[2]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"934b5cb7b56f417b05a73ff4e892e59a9a05dadc","Node name for S&R":"Runware Audio Model Search"},"widgets_values":["","Inworld","inworld:tts@2 (Inworld Realtime TTS 2)",false],"bgcolor":"#5345bf"},{"id":4,"type":"Runware Audio Inference Speech","pos":[10.43287294002262,67.55839196931291],"size":[400,454],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"voices","name":"voices","shape":7,"type":"RUNWAREAUDIOINFERENCESPEECHVOICES","link":null},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null},{"localized_name":"useVoice","name":"useVoice","shape":7,"type":"BOOLEAN","widget":{"name":"useVoice"},"link":null},{"localized_name":"voice","name":"voice","shape":7,"type":"STRING","widget":{"name":"voice"},"link":null},{"localized_name":"useSpeed","name":"useSpeed","shape":7,"type":"BOOLEAN","widget":{"name":"useSpeed"},"link":null},{"localized_name":"speed","name":"speed","shape":7,"type":"FLOAT","widget":{"name":"speed"},"link":null},{"localized_name":"useVolume","name":"useVolume","shape":7,"type":"BOOLEAN","widget":{"name":"useVolume"},"link":null},{"localized_name":"volume","name":"volume","shape":7,"type":"FLOAT","widget":{"name":"volume"},"link":null},{"localized_name":"usePitch","name":"usePitch","shape":7,"type":"BOOLEAN","widget":{"name":"usePitch"},"link":null},{"localized_name":"pitch","name":"pitch","shape":7,"type":"FLOAT","widget":{"name":"pitch"},"link":null},{"localized_name":"useEmotion","name":"useEmotion","shape":7,"type":"BOOLEAN","widget":{"name":"useEmotion"},"link":null},{"localized_name":"emotion","name":"emotion","shape":7,"type":"COMBO","widget":{"name":"emotion"},"link":null},{"localized_name":"useTone","name":"useTone","shape":7,"type":"BOOLEAN","widget":{"name":"useTone"},"link":null},{"localized_name":"tone","name":"tone","shape":7,"type":"STRING","widget":{"name":"tone"},"link":null},{"localized_name":"useLanguage","name":"useLanguage","shape":7,"type":"BOOLEAN","widget":{"name":"useLanguage"},"link":null},{"localized_name":"language","name":"language","shape":7,"type":"STRING","widget":{"name":"language"},"link":null}],"outputs":[{"localized_name":"speech","name":"speech","type":"RUNWARESPEECH","links":[3]}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"934b5cb7b56f417b05a73ff4e892e59a9a05dadc","Node name for S&R":"Runware Audio Inference Speech"},"widgets_values":["[speak quickly with a clear and direct manner] Your confirmation number is 8 4 7 2 9. Your order will arrive by FRIDAY at 3:45 PM. Total charged: forty-nine ninety-nine.",true,"Alex",true,1,false,1,false,0,false,"calm",false,"",false,"Auto"],"bgcolor":"#5345bf"},{"id":1,"type":"Runware Audio Inference","pos":[456.4160686947394,-174.43748067219647],"size":[400,746],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"RUNWAREAUDIOMODEL","link":2},{"localized_name":"inputs","name":"inputs","shape":7,"type":"RUNWAREAUDIOINFERENCEINPUTS","link":null},{"localized_name":"settings","name":"settings","shape":7,"type":"RUNWAREAUDIOSETTINGS","link":null},{"localized_name":"speech","name":"speech","shape":7,"type":"RUNWARESPEECH","link":3},{"localized_name":"voiceModify","name":"voiceModify","shape":7,"type":"RUNWAREVOICEMODIFY","link":null},{"localized_name":"providerSettings","name":"providerSettings","shape":7,"type":"RUNWAREPROVIDERSETTINGS","link":null},{"localized_name":"usePositivePrompt","name":"usePositivePrompt","type":"BOOLEAN","widget":{"name":"usePositivePrompt"},"link":null},{"localized_name":"positivePrompt","name":"positivePrompt","type":"STRING","widget":{"name":"positivePrompt"},"link":null},{"localized_name":"negativePrompt","name":"negativePrompt","type":"STRING","widget":{"name":"negativePrompt"},"link":null},{"localized_name":"useDuration","name":"useDuration","type":"BOOLEAN","widget":{"name":"useDuration"},"link":null},{"localized_name":"duration","name":"duration","type":"INT","widget":{"name":"duration"},"link":null},{"localized_name":"useSampleRate","name":"useSampleRate","type":"BOOLEAN","widget":{"name":"useSampleRate"},"link":null},{"localized_name":"sampleRate","name":"sampleRate","type":"COMBO","widget":{"name":"sampleRate"},"link":null},{"localized_name":"useBitrate","name":"useBitrate","type":"BOOLEAN","widget":{"name":"useBitrate"},"link":null},{"localized_name":"bitrate","name":"bitrate","type":"COMBO","widget":{"name":"bitrate"},"link":null},{"localized_name":"useChannels","name":"useChannels","type":"BOOLEAN","widget":{"name":"useChannels"},"link":null},{"localized_name":"channels","name":"channels","type":"INT","widget":{"name":"channels"},"link":null},{"localized_name":"outputType","name":"outputType","type":"COMBO","widget":{"name":"outputType"},"link":null},{"localized_name":"outputFormat","name":"outputFormat","type":"COMBO","widget":{"name":"outputFormat"},"link":null},{"localized_name":"numberResults","name":"numberResults","type":"INT","widget":{"name":"numberResults"},"link":null},{"localized_name":"useSeed","name":"useSeed","type":"BOOLEAN","widget":{"name":"useSeed"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"useSteps","name":"useSteps","type":"BOOLEAN","widget":{"name":"useSteps"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"useStrength","name":"useStrength","type":"BOOLEAN","widget":{"name":"useStrength"},"link":null},{"localized_name":"strength","name":"strength","type":"FLOAT","widget":{"name":"strength"},"link":null},{"localized_name":"useCFGScale","name":"useCFGScale","type":"BOOLEAN","widget":{"name":"useCFGScale"},"link":null},{"localized_name":"CFGScale","name":"CFGScale","type":"FLOAT","widget":{"name":"CFGScale"},"link":null}],"outputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","links":[1]},{"localized_name":"video","name":"video","type":"VIDEO","links":null}],"properties":{"cnr_id":"ComfyUI-Runware","ver":"934b5cb7b56f417b05a73ff4e892e59a9a05dadc","Node name for S&R":"Runware Audio Inference"},"widgets_values":[false,"classical piano piece, gentle and melodic","",false,10,false,32000,false,128,false,2,"URL","MP3",1,false,335374904,"randomize",false,20,false,0.8,false,12],"bgcolor":"#5345bf"}],"links":[[1,1,0,3,0,"AUDIO"],[2,2,0,1,0,"RUNWAREAUDIOMODEL"],[3,4,0,1,3,"RUNWARESPEECH"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.53,"offset":[625.7331058335624,492.72167878540404]}},"version":0.4} \ No newline at end of file