fix subtitles
This commit is contained in:
@@ -53,7 +53,7 @@ def parse_timestamp(timestamp_str):
|
||||
return 0.0
|
||||
|
||||
|
||||
def convert_subtitle_to_list(subtitle_string):
|
||||
def convert_subtitle_to_list(subtitle_string, fallback_duration):
|
||||
lines = subtitle_string.strip().split('\n')
|
||||
|
||||
parsed_entries = []
|
||||
@@ -94,12 +94,14 @@ def convert_subtitle_to_list(subtitle_string):
|
||||
next_start_time = parsed_entries[i+1]['start_time']
|
||||
end_time = next_start_time - 0.2
|
||||
else:
|
||||
fallback_duration = 1.5
|
||||
fallback_duration = fallback_duration
|
||||
end_time = start_time + fallback_duration
|
||||
|
||||
minimal_duration = 0.01 # A very small duration to avoid end_time <= start_time
|
||||
if end_time <= start_time:
|
||||
end_time = start_time + minimal_duration
|
||||
if end_time - start_time > 5:
|
||||
end_time = round(start_time + 5, 2)
|
||||
|
||||
# Append to result list
|
||||
result_list.append({'timestamp': [start_time, round(end_time,2)], 'text': text})
|
||||
@@ -169,8 +171,9 @@ class AddSubtitlesToTensor:
|
||||
subtitle_background_color = None
|
||||
|
||||
assert subtitle_text or json_text, "Either subtitle_text or json_text must be provided."
|
||||
if subtitle_text:
|
||||
subtitles = convert_subtitle_to_list(subtitle_text)
|
||||
fallback_duration = len(images) / fps - 0.2
|
||||
if subtitle_text:
|
||||
subtitles = convert_subtitle_to_list(subtitle_text, fallback_duration)
|
||||
else:
|
||||
subtitles = ast.literal_eval(json_text)
|
||||
|
||||
|
||||
+201
-80
@@ -101,6 +101,122 @@ def get_all_files(
|
||||
return file_dict if return_type == "dict" else file_paths
|
||||
|
||||
|
||||
from comfy.cli_args import args
|
||||
import av
|
||||
# from ComfyUI
|
||||
def save_audio(self, audio, filename_prefix="ComfyUI", format="flac", prompt=None, extra_pnginfo=None, quality="128k"):
|
||||
|
||||
filename_prefix += self.prefix_append
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
|
||||
results = []
|
||||
|
||||
# Prepare metadata dictionary
|
||||
metadata = {}
|
||||
if not args.disable_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 } }
|
||||
|
||||
|
||||
class SaveAudioMW:
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
@@ -110,8 +226,9 @@ class SaveAudioMW:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": { "audio": ("AUDIO", ),
|
||||
"format": (["WAV", "MP3", "FLAC"],),
|
||||
"format": (["MP3", "FLAC"],),
|
||||
"filename_prefix": ("STRING", {"default": "audio/ComfyUI"}),
|
||||
"mp3_quality": (["V0", "128k", "320k"],{"default": "V0"}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
@@ -123,98 +240,102 @@ class SaveAudioMW:
|
||||
|
||||
CATEGORY = "🎤MW/MW-Audio-Tools"
|
||||
|
||||
def save_audio(self, audio, format, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
|
||||
filename_prefix += self.prefix_append
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
|
||||
def save_audio(self, audio, format, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None, mp3_quality="128k"):
|
||||
|
||||
results = []
|
||||
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])
|
||||
return save_audio(self, audio, filename_prefix, format.lower(), prompt, extra_pnginfo, quality=mp3_quality)
|
||||
|
||||
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.lower()}"
|
||||
|
||||
buff = io.BytesIO()
|
||||
torchaudio.save(buff, waveform, audio["sample_rate"], format=format)
|
||||
# # --- Function to build a file tree structure ---
|
||||
# def build_file_tree(base_path, extensions, current_path=""):
|
||||
# """
|
||||
# Builds a recursive tree structure of files and directories.
|
||||
# Each node is a dictionary:
|
||||
# { "name": "...", "type": "folder" or "file", "path": "relative_path", "children": [...] }
|
||||
# """
|
||||
# tree = []
|
||||
# full_current_path = os.path.join(base_path, current_path)
|
||||
|
||||
# if not os.path.isdir(full_current_path):
|
||||
# return []
|
||||
|
||||
if format == "FLAC":
|
||||
buff = self.insert_or_replace_vorbis_comment(buff, metadata)
|
||||
elif metadata:
|
||||
print(("SaveAudioMW Warning: Metadata saving is currently only supported for FLAC format. "
|
||||
f"Skipping metadata for {format}"))
|
||||
# # Sort entries to have folders first, then files, alphabetically
|
||||
# entries = sorted(os.listdir(full_current_path))
|
||||
|
||||
# # Separate folders and files for processing
|
||||
# dirs = [d for d in entries if os.path.isdir(os.path.join(full_current_path, d))]
|
||||
# files = [f for f in entries if os.path.isfile(os.path.join(full_current_path, f))]
|
||||
|
||||
with open(os.path.join(full_output_folder, file), 'wb') as f:
|
||||
f.write(buff.getbuffer())
|
||||
# for name in dirs:
|
||||
# node_path = os.path.join(current_path, name)
|
||||
# children = build_file_tree(base_path, extensions, node_path)
|
||||
# # Only add folder if it has children or you want to show empty folders
|
||||
# if children: # or True, if you want to show empty folders
|
||||
# tree.append({
|
||||
# "name": name,
|
||||
# "type": "folder",
|
||||
# "path": os.path.normpath(node_path), # Store normalized relative path
|
||||
# "children": children
|
||||
# })
|
||||
|
||||
results.append({
|
||||
"filename": file,
|
||||
"subfolder": subfolder,
|
||||
"type": self.type
|
||||
})
|
||||
counter += 1
|
||||
# for name in files:
|
||||
# if any(name.lower().endswith(ext.lower()) for ext in extensions):
|
||||
# node_path = os.path.join(current_path, name)
|
||||
# tree.append({
|
||||
# "name": name,
|
||||
# "type": "file",
|
||||
# "path": os.path.normpath(node_path) # Store normalized relative path
|
||||
# })
|
||||
# return tree
|
||||
|
||||
return { "ui": { "audio": results },}
|
||||
|
||||
# From ComfyUI
|
||||
def create_vorbis_comment_block(self, comment_dict, last_block):
|
||||
vendor_string = b'ComfyUI'
|
||||
vendor_length = len(vendor_string)
|
||||
# class LoadAudioMW:
|
||||
# audios_dir_absolute = get_path()
|
||||
# _audio_extensions = [".wav", ".mp3", ".flac", ".mp4", ".WAV", ".MP3", ".FLAC", ".MP4"]
|
||||
# _file_tree_data = build_file_tree(audios_dir_absolute, _audio_extensions)
|
||||
# @classmethod
|
||||
# def INPUT_TYPES(s):
|
||||
# return {
|
||||
# "required": {
|
||||
# # The first element of the tuple is the data for our custom widget.
|
||||
# # The second element is a config dict. "widget": "HIERARCHICAL_FILE_AUDIO" is key.
|
||||
# "selected_audio_path": (s._file_tree_data, {"widget": "HIERARCHICAL_FILE_AUDIO"}),
|
||||
# "start_time_sec": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10000.0, "step": 0.01}),
|
||||
# "duration_sec": ("FLOAT", {"default": 5.0, "min": 0.01, "max": 10000.0, "step": 0.01}),
|
||||
# }
|
||||
# }
|
||||
|
||||
comments = []
|
||||
for key, value in comment_dict.items():
|
||||
comment = f"{key}={value}".encode('utf-8')
|
||||
comments.append(struct.pack('<I', len(comment)) + comment)
|
||||
# CATEGORY = "🎤MW/MW-Audio-Tools"
|
||||
# RETURN_TYPES = ("AUDIO",)
|
||||
# FUNCTION = "load"
|
||||
|
||||
user_comment_list_length = len(comments)
|
||||
user_comments = b''.join(comments)
|
||||
# def load(self, selected_audio_path, start_time_sec=0, duration_sec=5):
|
||||
# if not selected_audio_path or selected_audio_path.startswith("ERROR:"):
|
||||
# raise ValueError("No valid audio file selected or audio directory error.")
|
||||
|
||||
comment_data = struct.pack('<I', vendor_length) + vendor_string + struct.pack('<I', user_comment_list_length) + user_comments
|
||||
if last_block:
|
||||
id = b'\x84'
|
||||
else:
|
||||
id = b'\x04'
|
||||
comment_block = id + struct.pack('>I', len(comment_data))[1:] + comment_data
|
||||
# # selected_audio_path is the RELATIVE path from the JS widget
|
||||
# # We need to join it with the absolute base audio directory
|
||||
# if not LoadAudioMW.audios_dir_absolute:
|
||||
# raise FileNotFoundError("Audio base directory is not configured.")
|
||||
|
||||
# full_audio_path = os.path.join(LoadAudioMW.audios_dir_absolute, selected_audio_path)
|
||||
|
||||
return comment_block
|
||||
|
||||
# From ComfyUI
|
||||
def insert_or_replace_vorbis_comment(self, flac_io, comment_dict):
|
||||
if len(comment_dict) == 0:
|
||||
return flac_io
|
||||
|
||||
flac_io.seek(4)
|
||||
|
||||
blocks = []
|
||||
last_block = False
|
||||
|
||||
while not last_block:
|
||||
header = flac_io.read(4)
|
||||
last_block = (header[0] & 0x80) != 0
|
||||
block_type = header[0] & 0x7F
|
||||
block_length = struct.unpack('>I', b'\x00' + header[1:])[0]
|
||||
block_data = flac_io.read(block_length)
|
||||
|
||||
if block_type == 4 or block_type == 1:
|
||||
pass
|
||||
else:
|
||||
header = bytes([(header[0] & (~0x80))]) + header[1:]
|
||||
blocks.append(header + block_data)
|
||||
|
||||
blocks.append(self.create_vorbis_comment_block(comment_dict, last_block=True))
|
||||
|
||||
new_flac_io = io.BytesIO()
|
||||
new_flac_io.write(b'fLaC')
|
||||
for block in blocks:
|
||||
new_flac_io.write(block)
|
||||
|
||||
new_flac_io.write(flac_io.read())
|
||||
return new_flac_io
|
||||
# # --- Rest of your audio loading logic from the original example ---
|
||||
# # import torchaudio # Make sure torchaudio is imported
|
||||
# waveform, sample_rate = torchaudio.load(full_audio_path)
|
||||
# waveform = waveform.unsqueeze(0)
|
||||
|
||||
# start_sample = round(start_time_sec * sample_rate)
|
||||
# num_samples_requested = round(duration_sec * sample_rate)
|
||||
# total_samples = waveform.shape[-1]
|
||||
# start_sample = max(0, min(start_sample, total_samples))
|
||||
# actual_end_sample = min(start_sample + num_samples_requested, total_samples)
|
||||
|
||||
# if start_sample >= actual_end_sample:
|
||||
# raise ValueError(f"SliceAudio: Calculated slice is empty or invalid.")
|
||||
|
||||
# sliced_waveform = waveform[..., start_sample:actual_end_sample]
|
||||
# output_audio = {"waveform": sliced_waveform, "sample_rate": sample_rate}
|
||||
# return (output_audio,)
|
||||
|
||||
class LoadAudioMW:
|
||||
audios_dir = get_path()
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "audiotools"
|
||||
description = "A ComfyUI node containing multiple audio processing tools."
|
||||
version = "1.1.1"
|
||||
version = "1.1.2"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["sox", "librosa"]
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// // alert("Hierarchical Audio Widget JS Loaded!");
|
||||
// console.log("Hierarchical Audio Widget JS file is being processed.");
|
||||
|
||||
// // File: js/hierarchicalFileAudioWidget.js
|
||||
// import { app } from "../../scripts/app.js";
|
||||
// import { api } from "../../scripts/api.js";
|
||||
|
||||
// // Helper function to create tree elements
|
||||
// function createTreeElement(item, onFileSelect, currentPath) {
|
||||
// const element = document.createElement("li");
|
||||
// element.style.cursor = "pointer";
|
||||
// element.style.marginLeft = "10px"; // Basic indentation
|
||||
|
||||
// const nameSpan = document.createElement("span");
|
||||
// nameSpan.textContent = (item.type === "folder" ? "📁 " : "🎵 ") + item.name;
|
||||
// element.appendChild(nameSpan);
|
||||
|
||||
// if (item.type === "folder") {
|
||||
// nameSpan.style.fontWeight = "bold";
|
||||
// const childrenContainer = document.createElement("ul");
|
||||
// childrenContainer.style.display = "none"; // Collapsed by default
|
||||
// childrenContainer.style.listStyleType = "none";
|
||||
// childrenContainer.style.paddingLeft = "15px";
|
||||
|
||||
// if (item.children && item.children.length > 0) {
|
||||
// item.children.forEach(child => {
|
||||
// childrenContainer.appendChild(createTreeElement(child, onFileSelect, item.path));
|
||||
// });
|
||||
// } else {
|
||||
// const emptyMsg = document.createElement("li");
|
||||
// emptyMsg.textContent = "(empty)";
|
||||
// emptyMsg.style.fontStyle = "italic";
|
||||
// emptyMsg.style.color = "#888";
|
||||
// childrenContainer.appendChild(emptyMsg);
|
||||
// }
|
||||
// element.appendChild(childrenContainer);
|
||||
|
||||
// element.addEventListener("click", (e) => {
|
||||
// e.stopPropagation(); // Prevent event from bubbling up to parent folders
|
||||
// const isVisible = childrenContainer.style.display !== "none";
|
||||
// childrenContainer.style.display = isVisible ? "none" : "block";
|
||||
// nameSpan.textContent = (isVisible ? "📁 " : "📂 ") + item.name; // Change icon
|
||||
// });
|
||||
// } else { // File
|
||||
// element.addEventListener("click", (e) => {
|
||||
// e.stopPropagation();
|
||||
// onFileSelect(item.path); // Pass the relative path of the file
|
||||
// });
|
||||
// }
|
||||
// return element;
|
||||
// }
|
||||
|
||||
// if (app && typeof app.registerExtension === 'function') {
|
||||
// console.log("[MW_AUDIO_WIDGET] app.registerExtension IS a function. Attempting to register.");
|
||||
// app.registerExtension({
|
||||
// name: "MW.HierarchicalFileAudioWidget", // Unique name for your extension
|
||||
// // 添加 setup 函数
|
||||
// setup(appInstance) {
|
||||
// console.log("[MW_AUDIO_WIDGET] EXTENSION SETUP FUNCTION CALLED!");
|
||||
// },
|
||||
// async getCustomWidgets(appInstance) {
|
||||
// console.log("[MW_AUDIO_WIDGET] getCustomWidgets CALLED. Preparing to return object. (USING NORMAL FUNCTION)");
|
||||
|
||||
// const widgetsToRegister = {
|
||||
// "HIERARCHICAL_FILE_AUDIO": function(node, inputName, inputData, appRef) { // 改为普通函数
|
||||
// // 使用 'this' 关键字时要小心,在这个上下文中它可能不是你期望的
|
||||
// console.log(`[MW_AUDIO_WIDGET] NORMAL FUNCTION FACTORY for HIERARCHICAL_FILE_AUDIO CALLED! Input: ${inputName}`);
|
||||
// // ... (其余的工厂函数逻辑保持不变,使用空的或简化的版本进行测试)
|
||||
// let el = document.createElement("span");
|
||||
// el.textContent = "Normal Function Factory Called";
|
||||
// return { type: "STRING", name: inputName, value: "", element: el, computeSize: (w)=>[w,20] };
|
||||
// }
|
||||
// };
|
||||
|
||||
// console.log("[MW_AUDIO_WIDGET] Object being returned by getCustomWidgets:", widgetsToRegister);
|
||||
// return widgetsToRegister;
|
||||
// }
|
||||
// });
|
||||
// console.log("[MW_AUDIO_WIDGET] Extension registration attempt FINISHED.");
|
||||
// } else {
|
||||
// console.error("[MW_AUDIO_WIDGET] ERROR: app or app.registerExtension is not available!");
|
||||
// }
|
||||
Reference in New Issue
Block a user