Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
705e82af4a | ||
|
|
521aca76ee | ||
|
|
d3fe79b996 | ||
|
|
5f9c5f5dad | ||
|
|
b44f81e4d1 |
+22
-7
@@ -15,14 +15,14 @@ from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
|
||||
import folder_paths
|
||||
from aiohttp import web
|
||||
from server import PromptServer
|
||||
|
||||
from comfy_pack.hash import async_batch_get_sha256
|
||||
from comfy_pack.model_helper import alookup_model_source
|
||||
from comfy_pack.package import build_bento
|
||||
|
||||
import folder_paths
|
||||
from server import PromptServer
|
||||
|
||||
ZPath = Union[Path, zipfile.Path]
|
||||
TEMP_FOLDER = Path(__file__).parent.parent / "temp"
|
||||
COMFY_PACK_DIR = Path(__file__).parent.parent / "src" / "comfy_pack"
|
||||
@@ -132,16 +132,31 @@ async def _get_models(
|
||||
|
||||
models = []
|
||||
model_filenames = [
|
||||
os.path.abspath(line)
|
||||
os.path.abspath(line.strip().strip('"').replace('\\\\', '/'))
|
||||
for line in stdout.decode().splitlines()
|
||||
if not os.path.basename(line).startswith(".")
|
||||
if not os.path.basename(line.strip().strip('"').replace('\\\\', '/')).startswith(".")
|
||||
]
|
||||
|
||||
# Only compute hashes for referenced models
|
||||
to_include = []
|
||||
if model_filter:
|
||||
for m1 in model_filter:
|
||||
for m2 in model_filenames:
|
||||
if m1 in m2:
|
||||
to_include.append(m2)
|
||||
else:
|
||||
to_include = model_filenames
|
||||
|
||||
model_hashes = await async_batch_get_sha256(
|
||||
model_filenames,
|
||||
to_include,
|
||||
cache_only=not (ensure_sha or store_models),
|
||||
)
|
||||
|
||||
for filename in model_filenames:
|
||||
for filename in to_include:
|
||||
# Skip if file doesn't exist
|
||||
if not os.path.exists(filename):
|
||||
continue
|
||||
|
||||
relpath = os.path.relpath(filename, folder_paths.base_path)
|
||||
|
||||
model_data = {
|
||||
|
||||
+204
-5
@@ -1,5 +1,6 @@
|
||||
import glob
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@@ -11,8 +12,6 @@ import folder_paths
|
||||
import node_helpers
|
||||
import numpy as np
|
||||
import torch
|
||||
from comfy_extras.nodes_audio import SaveAudio
|
||||
from comfy_extras.nodes_video import SaveVideo
|
||||
from PIL import Image, ImageOps, ImageSequence, PngImagePlugin
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
|
||||
@@ -466,25 +465,225 @@ class OutputZip:
|
||||
return ()
|
||||
|
||||
|
||||
class OutputAudio(SaveAudio):
|
||||
class OutputAudio:
|
||||
CPACK_NODE = True
|
||||
CATEGORY = "ComfyPack/output"
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "save_audio"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "output"
|
||||
self.prefix_append = ""
|
||||
|
||||
def save_audio(
|
||||
self,
|
||||
audio,
|
||||
filename_prefix,
|
||||
format="flac",
|
||||
prompt=None,
|
||||
extra_pnginfo=None,
|
||||
quality="128k",
|
||||
):
|
||||
import av
|
||||
import torchaudio
|
||||
|
||||
filename_prefix += self.prefix_append
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = (
|
||||
get_save_image_path(filename_prefix, self.output_dir)
|
||||
)
|
||||
results = []
|
||||
|
||||
# Prepare metadata dictionary
|
||||
metadata = {}
|
||||
if prompt is not None:
|
||||
metadata["prompt"] = json.dumps(prompt)
|
||||
if extra_pnginfo is not None:
|
||||
for x in extra_pnginfo:
|
||||
metadata[x] = json.dumps(extra_pnginfo[x])
|
||||
|
||||
# Opus supported sample rates
|
||||
OPUS_RATES = [8000, 12000, 16000, 24000, 48000]
|
||||
|
||||
for batch_number, waveform in enumerate(audio["waveform"].cpu()):
|
||||
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
|
||||
file = f"{filename_with_batch_num}_{counter:05}.{format}"
|
||||
output_path = os.path.join(full_output_folder, file)
|
||||
|
||||
# Use original sample rate initially
|
||||
sample_rate = audio["sample_rate"]
|
||||
|
||||
# Handle Opus sample rate requirements
|
||||
if format == "opus":
|
||||
if sample_rate > 48000:
|
||||
sample_rate = 48000
|
||||
elif sample_rate not in OPUS_RATES:
|
||||
# Find the next highest supported rate
|
||||
for rate in sorted(OPUS_RATES):
|
||||
if rate > sample_rate:
|
||||
sample_rate = rate
|
||||
break
|
||||
if sample_rate not in OPUS_RATES: # Fallback if still not supported
|
||||
sample_rate = 48000
|
||||
|
||||
# Resample if necessary
|
||||
if sample_rate != audio["sample_rate"]:
|
||||
waveform = torchaudio.functional.resample(
|
||||
waveform, audio["sample_rate"], sample_rate
|
||||
)
|
||||
|
||||
# Create in-memory WAV buffer
|
||||
wav_buffer = io.BytesIO()
|
||||
torchaudio.save(wav_buffer, waveform, sample_rate, format="WAV")
|
||||
wav_buffer.seek(0) # Rewind for reading
|
||||
|
||||
# Use PyAV to convert and add metadata
|
||||
input_container = av.open(wav_buffer)
|
||||
|
||||
# Create output with specified format
|
||||
output_buffer = io.BytesIO()
|
||||
output_container = av.open(output_buffer, mode="w", format=format)
|
||||
|
||||
# Set metadata on the container
|
||||
for key, value in metadata.items():
|
||||
output_container.metadata[key] = value
|
||||
|
||||
# Set up the output stream with appropriate properties
|
||||
input_container.streams.audio[0]
|
||||
if format == "opus":
|
||||
out_stream = output_container.add_stream("libopus", rate=sample_rate)
|
||||
if quality == "64k":
|
||||
out_stream.bit_rate = 64000
|
||||
elif quality == "96k":
|
||||
out_stream.bit_rate = 96000
|
||||
elif quality == "128k":
|
||||
out_stream.bit_rate = 128000
|
||||
elif quality == "192k":
|
||||
out_stream.bit_rate = 192000
|
||||
elif quality == "320k":
|
||||
out_stream.bit_rate = 320000
|
||||
elif format == "mp3":
|
||||
out_stream = output_container.add_stream("libmp3lame", rate=sample_rate)
|
||||
if quality == "V0":
|
||||
# TODO i would really love to support V3 and V5 but there doesn't seem to be a way to set the qscale level, the property below is a bool
|
||||
out_stream.codec_context.qscale = 1
|
||||
elif quality == "128k":
|
||||
out_stream.bit_rate = 128000
|
||||
elif quality == "320k":
|
||||
out_stream.bit_rate = 320000
|
||||
else: # format == "flac":
|
||||
out_stream = output_container.add_stream("flac", rate=sample_rate)
|
||||
|
||||
# Copy frames from input to output
|
||||
for frame in input_container.decode(audio=0):
|
||||
frame.pts = None # Let PyAV handle timestamps
|
||||
output_container.mux(out_stream.encode(frame))
|
||||
|
||||
# Flush encoder
|
||||
output_container.mux(out_stream.encode(None))
|
||||
|
||||
# Close containers
|
||||
output_container.close()
|
||||
input_container.close()
|
||||
|
||||
# Write the output to file
|
||||
output_buffer.seek(0)
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(output_buffer.getbuffer())
|
||||
|
||||
results.append(
|
||||
{"filename": file, "subfolder": subfolder, "type": self.type}
|
||||
)
|
||||
counter += 1
|
||||
|
||||
return {"ui": {"audio": results}}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"audio": ("AUDIO",),
|
||||
"filename_prefix": ("STRING", {"default": "cpack_output_"}),
|
||||
"filename_prefix": ("STRING", {"default": "audio/comfypack"}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
|
||||
|
||||
class OutputVideo(SaveVideo):
|
||||
class OutputVideo:
|
||||
CPACK_NODE = True
|
||||
CATEGORY = "ComfyPack/output"
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "save_video"
|
||||
DESCRIPTION = "Saves the input video to your ComfyUI output directory."
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "output"
|
||||
self.prefix_append = ""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"video": ("VIDEO", {"tooltip": "The video to save."}),
|
||||
"filename_prefix": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "video/comfypack",
|
||||
"tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes.",
|
||||
},
|
||||
),
|
||||
"format": (
|
||||
["auto", "mp4"],
|
||||
{"default": "auto", "tooltip": "The format to save the video as."},
|
||||
),
|
||||
"codec": (
|
||||
["auto", "h264"],
|
||||
{"default": "auto", "tooltip": "The codec to use for the video."},
|
||||
),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
|
||||
def save_video(
|
||||
self,
|
||||
video,
|
||||
filename_prefix,
|
||||
format,
|
||||
codec,
|
||||
prompt=None,
|
||||
extra_pnginfo=None,
|
||||
):
|
||||
filename_prefix += self.prefix_append
|
||||
width, height = video.get_dimensions()
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = (
|
||||
get_save_image_path(filename_prefix, self.output_dir, width, height)
|
||||
)
|
||||
results = list()
|
||||
saved_metadata = None
|
||||
metadata = {}
|
||||
if extra_pnginfo is not None:
|
||||
metadata.update(extra_pnginfo)
|
||||
if prompt is not None:
|
||||
metadata["prompt"] = prompt
|
||||
if len(metadata) > 0:
|
||||
saved_metadata = metadata
|
||||
file = f"{filename}_{counter:05}.mp4"
|
||||
video.save_to(
|
||||
os.path.join(full_output_folder, file),
|
||||
format=format,
|
||||
codec=codec,
|
||||
metadata=saved_metadata,
|
||||
)
|
||||
|
||||
results.append({"filename": file, "subfolder": subfolder, "type": self.type})
|
||||
counter += 1
|
||||
|
||||
return {"ui": {"images": results, "animated": (True,)}}
|
||||
|
||||
|
||||
class OutputTextFile:
|
||||
@classmethod
|
||||
|
||||
@@ -113,7 +113,12 @@ async def async_batch_get_sha256(
|
||||
try:
|
||||
with SHA_CACHE_FILE.open("r") as f:
|
||||
cache = json.load(f)
|
||||
cache.update(new_cache)
|
||||
except (IOError, OSError):
|
||||
cache = {}
|
||||
|
||||
cache.update(new_cache)
|
||||
|
||||
try:
|
||||
with SHA_CACHE_FILE.open("w") as f:
|
||||
json.dump(cache, f, indent=2)
|
||||
except (IOError, OSError):
|
||||
|
||||
@@ -150,7 +150,7 @@ def install_dependencies(
|
||||
print("Using the current Python environment")
|
||||
venv_py = Path(sys.executable)
|
||||
else:
|
||||
venv = workspace / ".venv"
|
||||
venv = (workspace / ".venv").absolute()
|
||||
venv_py = (
|
||||
venv / "Scripts" / "python.exe"
|
||||
if os.name == "nt"
|
||||
|
||||
@@ -80,8 +80,9 @@ class ComfyService:
|
||||
logger = logging.getLogger("comfy_pack")
|
||||
logger.setLevel(logging.INFO)
|
||||
if not EXISTING_COMFYUI_SERVER:
|
||||
workspace_override = os.environ.get("COMFYUI_PATH", None)
|
||||
self.server = comfy_pack.run.ComfyUIServer(
|
||||
str(_get_workspace()),
|
||||
workspace_override if workspace_override else str(_get_workspace()),
|
||||
str(INPUT_DIR),
|
||||
verbose=int("BENTOML_DEBUG" in os.environ),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user