1.8.0
This commit is contained in:
@@ -1,18 +1,22 @@
|
|||||||
# noEmbryo Nodes
|
# noEmbryo Nodes
|
||||||
A diverse set of nodes for ComfyUI.
|
|
||||||
- [Json Prompt Loader](#json-prompt-loader)
|
|
||||||
- [Load Image (from path)](#load-image-from-path)
|
|
||||||
- [Resolution Scale](#resolution-scale)
|
|
||||||
- [Regex Text Chopper](#regex-text-chopper)
|
|
||||||
- [H3 Motion Context Clip Stitcher](#h3-motion-context-clip-stitcher)
|
|
||||||
- [H3 Motion Context Clip Purge](#h3-motion-context-clip-purge)
|
|
||||||
- [Auto Save Workflow](#auto-save-workflow)
|
|
||||||
- [PromptTermList (1-6)](#prompttermlist-1-6)
|
|
||||||
|
|
||||||
|
[![made-with-python][Python]](https://www.python.org/) [![License: MIT][MIT]](LICENSE)
|
||||||
|
A diverse set of nodes for ComfyUI.
|
||||||
You can access them through "Add node > noEmbryo" submenu.
|
You can access them through "Add node > noEmbryo" submenu.
|
||||||
|
|
||||||
[![made-with-python][Python]](https://www.python.org/)
|
* [Json Prompt Loader](#json-prompt-loader)
|
||||||
[![License: MIT][MIT]](LICENSE)
|
* [Load Image (from path)](#load-image-from-path)
|
||||||
|
* [Image Composer](#image-composer)
|
||||||
|
* [Resolution Scale](#resolution-scale)
|
||||||
|
* [Regex Text Chopper](#regex-text-chopper)
|
||||||
|
* [H3 Motion Context Clip Stitcher](#h3-motion-context-clip-stitcher)
|
||||||
|
* [H3 Clip Refiner](#h3-clip-refiner)
|
||||||
|
* [H3 Motion Context Clip Purge](#h3-motion-context-clip-purge)
|
||||||
|
* [H3 AV Latent from Video](#h3-av-latent-from-video)
|
||||||
|
* [H3 Context Latent Converter](#h3-context-latent-converter)
|
||||||
|
* [Auto Save Workflow](#auto-save-workflow)
|
||||||
|
* [PromptTermList 1-6](#prompttermlist-1-6)
|
||||||
|
* [Installation](#installation)
|
||||||
|
|
||||||
---
|
---
|
||||||
## Json Prompt Loader
|
## Json Prompt Loader
|
||||||
@@ -256,34 +260,6 @@ Encodes a whole video (IMAGE frames + AUDIO) with the MiniMax H3 VAEs into an AV
|
|||||||
A utility node that converts an `H3 Motion Context` archive latent (as loaded by `MiniMaxH3MotionContextLoadLatent`, whose 'samples' is a plain list) into the AV latent form that comfy-core's `LTXVSeparateAVLatent` expects (av_latent["samples"].unbind() -> (video, audio)).
|
A utility node that converts an `H3 Motion Context` archive latent (as loaded by `MiniMaxH3MotionContextLoadLatent`, whose 'samples' is a plain list) into the AV latent form that comfy-core's `LTXVSeparateAVLatent` expects (av_latent["samples"].unbind() -> (video, audio)).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Replace Audio no Re-Encode
|
|
||||||
|
|
||||||

|
|
||||||
A minimal ComfyUI custom node that replaces the audio stream of an existing video file with a new audio track, using ffmpeg's stream-copy mode for the video (`-c:v copy`). The video bitstream is remuxed losslessly and is never decoded/re-encoded, only the container is rewritten with a new audio stream. Requires ffmpeg to be installed and available on PATH.
|
|
||||||
|
|
||||||
- **Input**
|
|
||||||
- **audio**: ComfyUI AUDIO signal (e.g. from Load Audio or a generated audio node) to use as the new audio stream.
|
|
||||||
Ignored if audio_path is set.
|
|
||||||
|
|
||||||
- **Output**
|
|
||||||
- **video_path** returns the path where the video is saved in text.
|
|
||||||
|
|
||||||
- **Controls**
|
|
||||||
- **video_path** Path to the video file whose audio stream will be replaced (e.g. any .mp4/.mov/.mkv on disk).
|
|
||||||
- **filename_prefix** Prefix for the output file name.
|
|
||||||
The result is saved in the ComfyUI output directory as: <prefix>_<video name>_<counter>.<ext>
|
|
||||||
- **audio_codec** How to encode the new audio stream:
|
|
||||||
- aac: re-encode to AAC 192kbps (always used when the audio comes from the audio input)
|
|
||||||
- copy: remux the audio file losslessly, without re-encoding (only meaningful when using the audio_path input)"
|
|
||||||
- **audio_path** Path to an audio file, or a video file, whose audio stream will be extracted, to be used as the new audio stream.
|
|
||||||
If set, it takes priority over the audio input.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
## Auto Save Workflow
|
## Auto Save Workflow
|
||||||

|

|
||||||
|
|||||||
@@ -11,14 +11,14 @@ from .minimax import (H3MotionContextClipStitcher, H3ClipRefiner,
|
|||||||
H3MotionContextClipPurge, H3AVLatentFromVideo)
|
H3MotionContextClipPurge, H3AVLatentFromVideo)
|
||||||
|
|
||||||
MANIFEST = {"name": "noEmbryo Nodes",
|
MANIFEST = {"name": "noEmbryo Nodes",
|
||||||
"version": (1, 8, 0),
|
"version": (1, 8, 1),
|
||||||
"author": "noEmbryo",
|
"author": "noEmbryo",
|
||||||
"project": "https://github.com/noembryo/ComfyUI-noEmbryo",
|
"project": "https://github.com/noembryo/ComfyUI-noEmbryo",
|
||||||
"description": "Nodes for ComfyUI",
|
"description": "Nodes for ComfyUI",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
}
|
}
|
||||||
__author__ = "noEmbryo"
|
__author__ = "noEmbryo"
|
||||||
__version__ = "1.8.0"
|
__version__ = "1.8.1"
|
||||||
|
|
||||||
LISTS_PATH = join(dirname(realpath(__file__)), "TermLists")
|
LISTS_PATH = join(dirname(realpath(__file__)), "TermLists")
|
||||||
|
|
||||||
@@ -482,202 +482,202 @@ class AutoSaveWorkflow:
|
|||||||
return (status,)
|
return (status,)
|
||||||
|
|
||||||
|
|
||||||
class ReplaceAudioNoReEncode:
|
# class ReplaceAudioNoReEncode:
|
||||||
""" A minimal ComfyUI custom node that replaces the audio stream of an existing
|
# """ A minimal ComfyUI custom node that replaces the audio stream of an existing
|
||||||
video file with a new audio track, using ffmpeg's stream-copy mode for the
|
# video file with a new audio track, using ffmpeg's stream-copy mode for the
|
||||||
video (`-c:v copy`). The video bitstream is remuxed losslessly and is never
|
# video (`-c:v copy`). The video bitstream is remuxed losslessly and is never
|
||||||
decoded/re-encoded — only the container is rewritten with a new audio stream.
|
# decoded/re-encoded — only the container is rewritten with a new audio stream.
|
||||||
|
#
|
||||||
Requires ffmpeg to be installed and available on PATH.
|
# Requires ffmpeg to be installed and available on PATH.
|
||||||
|
#
|
||||||
video_path : path to an existing encoded video file (e.g. output of
|
# video_path : path to an existing encoded video file (e.g. output of
|
||||||
VHS Video Combine, or any .mp4/.mov/.mkv on disk).
|
# VHS Video Combine, or any .mp4/.mov/.mkv on disk).
|
||||||
audio : standard ComfyUI AUDIO type ({"waveform": tensor, "sample_rate": int}),
|
# audio : standard ComfyUI AUDIO type ({"waveform": tensor, "sample_rate": int}),
|
||||||
e.g. from Load Audio, VHS audio output, or a generated audio node.
|
# e.g. from Load Audio, VHS audio output, or a generated audio node.
|
||||||
"""
|
# """
|
||||||
|
#
|
||||||
@classmethod
|
# @classmethod
|
||||||
def INPUT_TYPES(cls):
|
# def INPUT_TYPES(cls):
|
||||||
return {
|
# return {
|
||||||
"required": {
|
# "required": {
|
||||||
"video_path": ("STRING", {"default": "", "multiline": False,
|
# "video_path": ("STRING", {"default": "", "multiline": False,
|
||||||
"tooltip": "Path to the video file whose audio stream "
|
# "tooltip": "Path to the video file whose audio stream "
|
||||||
"will be replaced (e.g. any .mp4/.mov/.mkv on disk)."}),
|
# "will be replaced (e.g. any .mp4/.mov/.mkv on disk)."}),
|
||||||
"filename_prefix": ("STRING", {"default": "audio_replaced",
|
# "filename_prefix": ("STRING", {"default": "audio_replaced",
|
||||||
"tooltip": "Prefix for the output file name.\n"
|
# "tooltip": "Prefix for the output file name.\n"
|
||||||
"The result is saved in the ComfyUI output "
|
# "The result is saved in the ComfyUI output "
|
||||||
"directory as:\n"
|
# "directory as:\n"
|
||||||
"<prefix>_<video name>_<counter>.<ext>"}),
|
# "<prefix>_<video name>_<counter>.<ext>"}),
|
||||||
"audio_codec": (["aac", "copy"], {"default": "aac",
|
# "audio_codec": (["aac", "copy"], {"default": "aac",
|
||||||
"tooltip": "How to encode the new audio stream:\n"
|
# "tooltip": "How to encode the new audio stream:\n"
|
||||||
"• aac: re-encode to AAC 192kbps (always "
|
# "• aac: re-encode to AAC 192kbps (always "
|
||||||
"used when the audio comes from the AUDIO "
|
# "used when the audio comes from the AUDIO "
|
||||||
"tensor input)\n"
|
# "tensor input)\n"
|
||||||
"• copy: remux the audio file losslessly, "
|
# "• copy: remux the audio file losslessly, "
|
||||||
"without re-encoding (only meaningful when "
|
# "without re-encoding (only meaningful when "
|
||||||
"using the audio_path input)"}),
|
# "using the audio_path input)"}),
|
||||||
},
|
# },
|
||||||
"optional": {
|
# "optional": {
|
||||||
"audio": ("AUDIO", {"tooltip": "ComfyUI AUDIO signal (e.g. from Load Audio or a "
|
# "audio": ("AUDIO", {"tooltip": "ComfyUI AUDIO signal (e.g. from Load Audio or a "
|
||||||
"generated audio node) to use as the new audio "
|
# "generated audio node) to use as the new audio "
|
||||||
"stream.\nIgnored if audio_path is set."}),
|
# "stream.\nIgnored if audio_path is set."}),
|
||||||
"audio_path": ("STRING", {"default": "", "multiline": False,
|
# "audio_path": ("STRING", {"default": "", "multiline": False,
|
||||||
"tooltip": "Path to an audio file — or a video file, whose "
|
# "tooltip": "Path to an audio file — or a video file, whose "
|
||||||
"audio stream will be extracted — to use as the new "
|
# "audio stream will be extracted — to use as the new "
|
||||||
"audio stream. If set, it takes priority over the "
|
# "audio stream. If set, it takes priority over the "
|
||||||
"audio tensor input."}),
|
# "audio tensor input."}),
|
||||||
"shortest": ("BOOLEAN", {"default": True,
|
# "shortest": ("BOOLEAN", {"default": True,
|
||||||
"tooltip": "If enabled and the audio is shorter/longer than "
|
# "tooltip": "If enabled and the audio is shorter/longer than "
|
||||||
"the video, the output is trimmed to the "
|
# "the video, the output is trimmed to the "
|
||||||
"shorter of the two streams."}),
|
# "shorter of the two streams."}),
|
||||||
},
|
# },
|
||||||
"hidden": {
|
# "hidden": {
|
||||||
"prompt": "PROMPT",
|
# "prompt": "PROMPT",
|
||||||
"extra_pnginfo": "EXTRA_PNGINFO",
|
# "extra_pnginfo": "EXTRA_PNGINFO",
|
||||||
},
|
# },
|
||||||
}
|
# }
|
||||||
|
#
|
||||||
DESCRIPTION = ("Replaces the audio stream of a video file without re-encoding the video. "
|
# DESCRIPTION = ("Replaces the audio stream of a video file without re-encoding the video. "
|
||||||
"The new audio comes either from an AUDIO tensor input or from an audio file "
|
# "The new audio comes either from an AUDIO tensor input or from an audio file "
|
||||||
"given by audio_path. Requires ffmpeg on the PATH.")
|
# "given by audio_path. Requires ffmpeg on the PATH.")
|
||||||
|
#
|
||||||
RETURN_TYPES = ("STRING",)
|
# RETURN_TYPES = ("STRING",)
|
||||||
RETURN_NAMES = ("video_path",)
|
# RETURN_NAMES = ("video_path",)
|
||||||
OUTPUT_TOOLTIPS = ("The path of the output video file with the replaced audio stream.",)
|
# OUTPUT_TOOLTIPS = ("The path of the output video file with the replaced audio stream.",)
|
||||||
FUNCTION = "replace_audio"
|
# FUNCTION = "replace_audio"
|
||||||
CATEGORY = "noEmbryo"
|
# CATEGORY = "noEmbryo"
|
||||||
OUTPUT_NODE = True
|
# OUTPUT_NODE = True
|
||||||
|
#
|
||||||
@staticmethod
|
# @staticmethod
|
||||||
def _ffm_escape(text):
|
# def _ffm_escape(text):
|
||||||
""" Escapes a string for use as a value in an ffmetadata file """
|
# """ Escapes a string for use as a value in an ffmetadata file """
|
||||||
for ch in ("\\", "=", ";", "#", "\n"):
|
# for ch in ("\\", "=", ";", "#", "\n"):
|
||||||
text = text.replace(ch, "\\" + ch) if ch != "\n" else text.replace(ch, r"\n")
|
# text = text.replace(ch, "\\" + ch) if ch != "\n" else text.replace(ch, r"\n")
|
||||||
return text
|
# return text
|
||||||
|
#
|
||||||
@staticmethod
|
# @staticmethod
|
||||||
def write_wave_file(wave_path, waveform, sample_rate):
|
# def write_wave_file(wave_path, waveform, sample_rate):
|
||||||
""" Writes a waveform tensor to a wav file, using only the standard library
|
# """ Writes a waveform tensor to a wav file, using only the standard library
|
||||||
"""
|
# """
|
||||||
import wave
|
# import wave
|
||||||
import numpy as np
|
# import numpy as np
|
||||||
if waveform.dim() == 1: # [samples] -> [1, samples]
|
# if waveform.dim() == 1: # [samples] -> [1, samples]
|
||||||
waveform = waveform.unsqueeze(0)
|
# waveform = waveform.unsqueeze(0)
|
||||||
# [channels, samples] -> [samples, channels]
|
# # [channels, samples] -> [samples, channels]
|
||||||
samples = waveform.cpu().numpy().T
|
# samples = waveform.cpu().numpy().T
|
||||||
samples = np.clip(samples, -1.0, 1.0)
|
# samples = np.clip(samples, -1.0, 1.0)
|
||||||
pcm = (samples * 32767.0).astype(np.int16)
|
# pcm = (samples * 32767.0).astype(np.int16)
|
||||||
with wave.open(wave_path, "wb") as wf:
|
# with wave.open(wave_path, "wb") as wf:
|
||||||
wf.setnchannels(pcm.shape[1])
|
# wf.setnchannels(pcm.shape[1])
|
||||||
wf.setsampwidth(2) # 2 bytes = 16 bit
|
# wf.setsampwidth(2) # 2 bytes = 16 bit
|
||||||
wf.setframerate(sample_rate)
|
# wf.setframerate(sample_rate)
|
||||||
wf.writeframes(pcm.tobytes())
|
# wf.writeframes(pcm.tobytes())
|
||||||
|
#
|
||||||
def replace_audio(self, video_path, filename_prefix, audio_codec,
|
# def replace_audio(self, video_path, filename_prefix, audio_codec,
|
||||||
audio=None, audio_path="", shortest=True,
|
# audio=None, audio_path="", shortest=True,
|
||||||
prompt=None, extra_pnginfo=None):
|
# prompt=None, extra_pnginfo=None):
|
||||||
if not video_path or not os.path.isfile(video_path):
|
# if not video_path or not os.path.isfile(video_path):
|
||||||
raise FileNotFoundError(f"Video file not found: {video_path!r}")
|
# raise FileNotFoundError(f"Video file not found: {video_path!r}")
|
||||||
|
#
|
||||||
output_dir = folder_paths.get_output_directory()
|
# output_dir = folder_paths.get_output_directory()
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
# os.makedirs(output_dir, exist_ok=True)
|
||||||
|
#
|
||||||
tmp_audio_path = None
|
# tmp_audio_path = None
|
||||||
if audio_path:
|
# if audio_path:
|
||||||
if not os.path.isfile(audio_path):
|
# if not os.path.isfile(audio_path):
|
||||||
raise FileNotFoundError(f"Audio file not found: {audio_path!r}")
|
# raise FileNotFoundError(f"Audio file not found: {audio_path!r}")
|
||||||
second_input = audio_path
|
# second_input = audio_path
|
||||||
elif audio is not None:
|
# elif audio is not None:
|
||||||
# --- Write the incoming AUDIO tensor to a temp wav file ---
|
# # --- Write the incoming AUDIO tensor to a temp wav file ---
|
||||||
waveform = audio["waveform"]
|
# waveform = audio["waveform"]
|
||||||
sample_rate = audio["sample_rate"]
|
# sample_rate = audio["sample_rate"]
|
||||||
if waveform.dim() == 3: # [batch, channels, samples] -> take first item
|
# if waveform.dim() == 3: # [batch, channels, samples] -> take first item
|
||||||
waveform = waveform[0]
|
# waveform = waveform[0]
|
||||||
tmp_audio_fd, tmp_audio_path = tempfile.mkstemp(suffix=".wav")
|
# tmp_audio_fd, tmp_audio_path = tempfile.mkstemp(suffix=".wav")
|
||||||
os.close(tmp_audio_fd)
|
# os.close(tmp_audio_fd)
|
||||||
self.write_wave_file(tmp_audio_path, waveform, sample_rate)
|
# self.write_wave_file(tmp_audio_path, waveform, sample_rate)
|
||||||
second_input = tmp_audio_path
|
# second_input = tmp_audio_path
|
||||||
# Copying raw PCM into a container makes no sense, so force aac
|
# # Copying raw PCM into a container makes no sense, so force aac
|
||||||
audio_codec = "aac"
|
# audio_codec = "aac"
|
||||||
else:
|
# else:
|
||||||
raise ValueError("No audio given: connect an AUDIO input or set audio_path.")
|
# raise ValueError("No audio given: connect an AUDIO input or set audio_path.")
|
||||||
|
#
|
||||||
# --- Build a unique output path ---
|
# # --- Build a unique output path ---
|
||||||
base_name = os.path.splitext(os.path.basename(video_path))[0]
|
# base_name = os.path.splitext(os.path.basename(video_path))[0]
|
||||||
ext = os.path.splitext(video_path)[1] or ".mp4"
|
# ext = os.path.splitext(video_path)[1] or ".mp4"
|
||||||
# Start from max existing number + 1, so deleted files don't cause name reuse.
|
# # Start from max existing number + 1, so deleted files don't cause name reuse.
|
||||||
# The prefix may contain subdirectories (e.g. "MMH3\NewAudio"), so the scan
|
# # The prefix may contain subdirectories (e.g. "MMH3\NewAudio"), so the scan
|
||||||
# must look in the directory the files are actually written to.
|
# # must look in the directory the files are actually written to.
|
||||||
out_path = os.path.join(output_dir, f"{filename_prefix}_{base_name}_001{ext}")
|
# out_path = os.path.join(output_dir, f"{filename_prefix}_{base_name}_001{ext}")
|
||||||
scan_dir = os.path.dirname(out_path)
|
# scan_dir = os.path.dirname(out_path)
|
||||||
os.makedirs(scan_dir, exist_ok=True)
|
# os.makedirs(scan_dir, exist_ok=True)
|
||||||
# listdir() returns bare filenames, so only the last component of the
|
# # listdir() returns bare filenames, so only the last component of the
|
||||||
# prefix (without the directory part) can appear in them
|
# # prefix (without the directory part) can appear in them
|
||||||
prefix_name = os.path.basename(filename_prefix.replace("\\", "/"))
|
# prefix_name = os.path.basename(filename_prefix.replace("\\", "/"))
|
||||||
counter = 1
|
# counter = 1
|
||||||
pattern = re.compile(rf"^{re.escape(prefix_name)}_{re.escape(base_name)}"
|
# pattern = re.compile(rf"^{re.escape(prefix_name)}_{re.escape(base_name)}"
|
||||||
rf"_(\d+){re.escape(ext)}$")
|
# rf"_(\d+){re.escape(ext)}$")
|
||||||
for fname in os.listdir(scan_dir):
|
# for fname in os.listdir(scan_dir):
|
||||||
m = pattern.match(fname)
|
# m = pattern.match(fname)
|
||||||
if m:
|
# if m:
|
||||||
counter = max(counter, int(m.group(1)) + 1)
|
# counter = max(counter, int(m.group(1)) + 1)
|
||||||
out_name = f"{filename_prefix}_{base_name}_{counter:03d}{ext}"
|
# out_name = f"{filename_prefix}_{base_name}_{counter:03d}{ext}"
|
||||||
out_path = os.path.join(output_dir, out_name)
|
# out_path = os.path.join(output_dir, out_name)
|
||||||
|
#
|
||||||
# --- Write the workflow metadata to a temp ffmetadata file ---
|
# # --- Write the workflow metadata to a temp ffmetadata file ---
|
||||||
# (avoids Windows command-line length limits that -metadata args would hit)
|
# # (avoids Windows command-line length limits that -metadata args would hit)
|
||||||
meta_fd, meta_path = tempfile.mkstemp(suffix=".txt")
|
# meta_fd, meta_path = tempfile.mkstemp(suffix=".txt")
|
||||||
os.close(meta_fd)
|
# os.close(meta_fd)
|
||||||
with io.open(meta_path, "w", encoding="utf-8") as mf:
|
# with io.open(meta_path, "w", encoding="utf-8") as mf:
|
||||||
mf.write(";FFMETADATA1\n")
|
# mf.write(";FFMETADATA1\n")
|
||||||
if prompt is not None:
|
# if prompt is not None:
|
||||||
mf.write(f"prompt={self._ffm_escape(json.dumps(prompt))}\n")
|
# mf.write(f"prompt={self._ffm_escape(json.dumps(prompt))}\n")
|
||||||
if extra_pnginfo and "workflow" in extra_pnginfo:
|
# if extra_pnginfo and "workflow" in extra_pnginfo:
|
||||||
mf.write(f"workflow={self._ffm_escape(json.dumps(extra_pnginfo['workflow']))}\n")
|
# mf.write(f"workflow={self._ffm_escape(json.dumps(extra_pnginfo['workflow']))}\n")
|
||||||
|
#
|
||||||
# --- ffmpeg: stream-copy the video, only touch the audio ---
|
# # --- ffmpeg: stream-copy the video, only touch the audio ---
|
||||||
cmd = [
|
# cmd = [
|
||||||
"ffmpeg", "-y",
|
# "ffmpeg", "-y",
|
||||||
"-i", video_path,
|
# "-i", video_path,
|
||||||
"-i", second_input,
|
# "-i", second_input,
|
||||||
"-i", meta_path,
|
# "-i", meta_path,
|
||||||
"-map", "0:v:0",
|
# "-map", "0:v:0",
|
||||||
"-map", "1:a:0",
|
# "-map", "1:a:0",
|
||||||
"-map_metadata", "2",
|
# "-map_metadata", "2",
|
||||||
"-c:v", "copy",
|
# "-c:v", "copy",
|
||||||
]
|
# ]
|
||||||
if audio_codec == "copy":
|
# if audio_codec == "copy":
|
||||||
cmd += ["-c:a", "copy"]
|
# cmd += ["-c:a", "copy"]
|
||||||
else:
|
# else:
|
||||||
cmd += ["-c:a", "aac", "-b:a", "192k"]
|
# cmd += ["-c:a", "aac", "-b:a", "192k"]
|
||||||
# allow arbitrary metadata keys in these containers
|
# # allow arbitrary metadata keys in these containers
|
||||||
if ext.lower() in (".mp4", ".mov"):
|
# if ext.lower() in (".mp4", ".mov"):
|
||||||
cmd += ["-movflags", "use_metadata_tags"]
|
# cmd += ["-movflags", "use_metadata_tags"]
|
||||||
if shortest:
|
# if shortest:
|
||||||
cmd.append("-shortest")
|
# cmd.append("-shortest")
|
||||||
cmd.append(out_path)
|
# cmd.append(out_path)
|
||||||
|
#
|
||||||
def run_ffmpeg(command):
|
# def run_ffmpeg(command):
|
||||||
return subprocess.run(command, capture_output=True, text=True)
|
# return subprocess.run(command, capture_output=True, text=True)
|
||||||
|
#
|
||||||
try:
|
# try:
|
||||||
result = run_ffmpeg(cmd)
|
# result = run_ffmpeg(cmd)
|
||||||
if result.returncode != 0 and audio_codec == "copy":
|
# if result.returncode != 0 and audio_codec == "copy":
|
||||||
# "copy" can fail when the source audio codec is incompatible with
|
# # "copy" can fail when the source audio codec is incompatible with
|
||||||
# the output container (e.g. PCM in an AVI -> mp4). Retry with aac.
|
# # the output container (e.g. PCM in an AVI -> mp4). Retry with aac.
|
||||||
fallback_cmd = list(cmd)
|
# fallback_cmd = list(cmd)
|
||||||
for i, arg in enumerate(fallback_cmd):
|
# for i, arg in enumerate(fallback_cmd):
|
||||||
if arg == "-c:a" and fallback_cmd[i + 1] == "copy":
|
# if arg == "-c:a" and fallback_cmd[i + 1] == "copy":
|
||||||
fallback_cmd[i + 1] = "aac"
|
# fallback_cmd[i + 1] = "aac"
|
||||||
result = run_ffmpeg(fallback_cmd)
|
# result = run_ffmpeg(fallback_cmd)
|
||||||
if result.returncode != 0:
|
# if result.returncode != 0:
|
||||||
raise RuntimeError(f"ffmpeg failed (exit {result.returncode}):\n{result.stderr}")
|
# raise RuntimeError(f"ffmpeg failed (exit {result.returncode}):\n{result.stderr}")
|
||||||
finally:
|
# finally:
|
||||||
for tmp in (tmp_audio_path, meta_path):
|
# for tmp in (tmp_audio_path, meta_path):
|
||||||
if tmp and os.path.exists(tmp):
|
# if tmp and os.path.exists(tmp):
|
||||||
os.remove(tmp)
|
# os.remove(tmp)
|
||||||
|
#
|
||||||
return (out_path,)
|
# return (out_path,)
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {f"JsonPromptLoader -{__author__}": JsonPromptLoader,
|
NODE_CLASS_MAPPINGS = {f"JsonPromptLoader -{__author__}": JsonPromptLoader,
|
||||||
@@ -691,7 +691,7 @@ NODE_CLASS_MAPPINGS = {f"JsonPromptLoader -{__author__}": JsonPromptLoader,
|
|||||||
f"H3MotionContextClipPurge -{__author__}": H3MotionContextClipPurge,
|
f"H3MotionContextClipPurge -{__author__}": H3MotionContextClipPurge,
|
||||||
f"H3ContextLatentConverter -{__author__}": H3ContextLatentConverter,
|
f"H3ContextLatentConverter -{__author__}": H3ContextLatentConverter,
|
||||||
f"H3AVLatentFromVideo -{__author__}": H3AVLatentFromVideo,
|
f"H3AVLatentFromVideo -{__author__}": H3AVLatentFromVideo,
|
||||||
f"ReplaceAudioNoReEncode -{__author__}": ReplaceAudioNoReEncode,
|
# f"ReplaceAudioNoReEncode -{__author__}": ReplaceAudioNoReEncode,
|
||||||
"PromptTermList1": PromptTermList1,
|
"PromptTermList1": PromptTermList1,
|
||||||
"PromptTermList2": PromptTermList2,
|
"PromptTermList2": PromptTermList2,
|
||||||
"PromptTermList3": PromptTermList3,
|
"PromptTermList3": PromptTermList3,
|
||||||
@@ -711,7 +711,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {f"JsonPromptLoader -{__author__}": f"Json Prompt L
|
|||||||
f"H3MotionContextClipPurge -{__author__}": f"H3 Motion Context Clip Purge /{__author__}",
|
f"H3MotionContextClipPurge -{__author__}": f"H3 Motion Context Clip Purge /{__author__}",
|
||||||
f"H3ContextLatentConverter -{__author__}": f"H3 Context Latent Converter /{__author__}",
|
f"H3ContextLatentConverter -{__author__}": f"H3 Context Latent Converter /{__author__}",
|
||||||
f"H3AVLatentFromVideo -{__author__}": f"H3 AV Latent from Video /{__author__}",
|
f"H3AVLatentFromVideo -{__author__}": f"H3 AV Latent from Video /{__author__}",
|
||||||
f"ReplaceAudioNoReEncode -{__author__}": f"Replace Audio no ReEncode /{__author__}",
|
# f"ReplaceAudioNoReEncode -{__author__}": f"Replace Audio no ReEncode /{__author__}",
|
||||||
"PromptTermList1": f"PromptTermList 1 /{__author__}",
|
"PromptTermList1": f"PromptTermList 1 /{__author__}",
|
||||||
"PromptTermList2": f"PromptTermList 2 /{__author__}",
|
"PromptTermList2": f"PromptTermList 2 /{__author__}",
|
||||||
"PromptTermList3": f"PromptTermList 3 /{__author__}",
|
"PromptTermList3": f"PromptTermList 3 /{__author__}",
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ limit the output's size in megapixels. \n
|
|||||||
* "PromptTermList (1-6)" nodes to help with the creation of Prompts. \n
|
* "PromptTermList (1-6)" nodes to help with the creation of Prompts. \n
|
||||||
* "Auto Save Workflow" can save the current workflow to a .json file automatically. \n
|
* "Auto Save Workflow" can save the current workflow to a .json file automatically. \n
|
||||||
"""
|
"""
|
||||||
version = "1.8.0"
|
version = "1.8.1"
|
||||||
license = {text = "MIT License"}
|
license = {text = "MIT License"}
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user