commit cba05fb103bb216dc66ba54590261bd951cffbf3 Author: Li Jian <4362378@qq.com> Date: Sat Jul 19 22:36:31 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6df3b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +*.pyw +*.pyz +*.pywz +*.pyzw +*.pyzwz +*.pyzwzw \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8278bee --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# ComfyUI WB Audio Duration Node + +一个简单的ComfyUI自定义节点,用于获取音频文件的时长。 + +## 功能 + +- 支持1D, 2D, 3D音频张量格式 +- 支持 `waveform` 和 `audio` 键格式 +- 提供详细的调试信息 +- 格式化时长显示(秒、分、小时) + +## 安装 + +1. 将此文件夹复制到ComfyUI的 `custom_nodes` 目录 +2. 重启ComfyUI +3. 在节点列表中找到 "WB Audio Duration" + +## 使用方法 + +1. 添加 "Load Audio" 节点加载音频文件 +2. 添加 "WB Audio Duration" 节点 +3. 连接音频输出到时长节点 +4. 运行工作流查看结果 + +## 输出 + +- `duration_seconds`: 时长(秒,浮点数) +- `duration_text`: 格式化的时长文本 + +## 支持格式 + +- 1D张量: `[samples]` +- 2D张量: `[channels, samples]` 或 `[batch, samples]` +- 3D张量: `[batch, channels, samples]` + +## 调试信息 + +节点会在控制台输出详细的调试信息,包括: +- 音频输入类型 +- 音频字典键 +- 音频数据形状 +- 计算过程 \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..80ec3d4 --- /dev/null +++ b/__init__.py @@ -0,0 +1,161 @@ +# ComfyUI Audio Duration Node Package + +# 直接定义节点类,避免导入问题 +import os +import sys +import folder_paths + +# 尝试导入torch,如果失败则使用备用方法 +try: + import torch + TORCH_AVAILABLE = True +except ImportError: + TORCH_AVAILABLE = False + print("警告: torch未安装,将使用备用方法处理音频数据") + +class WB_AudioDurationNode: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.output_node = True + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "audio": ("AUDIO",), + }, + } + + RETURN_TYPES = ("FLOAT", "STRING",) + RETURN_NAMES = ("duration_seconds", "duration_text",) + FUNCTION = "get_duration" + OUTPUT_NODE = True + CATEGORY = "audio" + + def get_duration(self, audio): + """ + 简化的音频时长获取函数 + 支持1D, 2D, 3D张量格式 + """ + try: + print(f"=== 音频分析开始 ===") + print(f"音频输入类型: {type(audio)}") + + # 处理字典格式 + if isinstance(audio, dict): + print(f"音频字典键: {list(audio.keys())}") + + # 检查是否有必要的键 - 支持两种格式 + if ('waveform' in audio and 'sample_rate' in audio) or ('audio' in audio and 'sample_rate' in audio): + # 优先使用waveform,如果没有则使用audio + if 'waveform' in audio: + audio_data = audio['waveform'] + print("使用waveform键") + else: + audio_data = audio['audio'] + print("使用audio键") + + sample_rate = audio['sample_rate'] + + print(f"音频数据类型: {type(audio_data)}") + print(f"采样率: {sample_rate}") + + # 尝试获取音频长度 + if hasattr(audio_data, 'shape'): + print(f"音频数据形状: {audio_data.shape}") + + # 计算样本数 - 支持1D, 2D, 3D张量 + if len(audio_data.shape) == 1: + # 1D: [samples] + num_samples = audio_data.shape[0] + print("处理1D张量") + elif len(audio_data.shape) == 2: + # 2D: [channels, samples] 或 [batch, samples] + num_samples = audio_data.shape[1] + print("处理2D张量") + elif len(audio_data.shape) == 3: + # 3D: [batch, channels, samples] + num_samples = audio_data.shape[2] + print("处理3D张量") + else: + return (0.0, f"不支持的形状: {audio_data.shape}") + + # 计算时长 + duration_seconds = num_samples / sample_rate + duration_text = self.format_duration(duration_seconds) + + print(f"样本数: {num_samples}") + print(f"计算出的时长: {duration_seconds}秒") + print(f"格式化时长: {duration_text}") + + return (duration_seconds, duration_text) + + elif hasattr(audio_data, '__len__'): + # 如果数据有长度属性 + num_samples = len(audio_data) + duration_seconds = num_samples / sample_rate + duration_text = self.format_duration(duration_seconds) + + print(f"使用长度属性,样本数: {num_samples}") + print(f"计算出的时长: {duration_seconds}秒") + + return (duration_seconds, duration_text) + + else: + return (0.0, f"无法获取音频数据长度,类型: {type(audio_data)}") + + else: + return (0.0, f"音频字典缺少必要键,当前键: {list(audio.keys())}") + + # 处理字符串(文件路径) + elif isinstance(audio, str): + print(f"音频文件路径: {audio}") + if os.path.exists(audio): + # 简单估算 + file_size = os.path.getsize(audio) + estimated_duration = (file_size * 8) / (128 * 1000) # 假设128kbps + duration_text = self.format_duration(estimated_duration) + return (estimated_duration, f"估算时长: {duration_text}") + else: + return (0.0, f"文件不存在: {audio}") + + else: + return (0.0, f"不支持的音频输入类型: {type(audio)}") + + except Exception as e: + import traceback + print(f"=== 错误详情 ===") + print(f"错误类型: {type(e)}") + print(f"错误信息: {str(e)}") + print(f"错误堆栈: {traceback.format_exc()}") + return (0.0, f"处理错误: {str(e)}") + + def format_duration(self, seconds): + """ + 格式化时长显示 + """ + if seconds < 1: + return f"{seconds:.3f}秒" + elif seconds < 60: + return f"{seconds:.2f}秒" + elif seconds < 3600: + minutes = int(seconds // 60) + remaining_seconds = seconds % 60 + return f"{minutes}分{remaining_seconds:.2f}秒" + else: + hours = int(seconds // 3600) + remaining_minutes = int((seconds % 3600) // 60) + remaining_seconds = seconds % 60 + return f"{hours}小时{remaining_minutes}分{remaining_seconds:.2f}秒" + +# 节点注册 +NODE_CLASS_MAPPINGS = { + "WB_AudioDuration": WB_AudioDurationNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "WB_AudioDuration": "WB Audio Duration" +} + +print("ComfyUI WB Audio Duration Node 已加载") \ No newline at end of file diff --git a/workflow_sample.json b/workflow_sample.json new file mode 100644 index 0000000..9eb753e --- /dev/null +++ b/workflow_sample.json @@ -0,0 +1,185 @@ +{ + "id": "dfb2747d-8793-4209-b89b-c92284762d7d", + "revision": 0, + "last_node_id": 6, + "last_link_id": 4, + "nodes": [ + { + "id": 2, + "type": "LoadAudio", + "pos": [ + 1446.5673828125, + -2019.3924560546875 + ], + "size": [ + 270, + 136 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "AUDIO", + "type": "AUDIO", + "links": [ + 1 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.44", + "Node name for S&R": "LoadAudio" + }, + "widgets_values": [ + "song.mp3", + null, + null + ] + }, + { + "id": 3, + "type": "WB_AudioDuration", + "pos": [ + 1782.639892578125, + -1973.1875 + ], + "size": [ + 169.095703125, + 46 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "AUDIO", + "link": 1 + } + ], + "outputs": [ + { + "name": "duration_seconds", + "type": "FLOAT", + "links": [ + 3 + ] + }, + { + "name": "duration_text", + "type": "STRING", + "links": [ + 4 + ] + } + ], + "properties": { + "Node name for S&R": "WB_AudioDuration" + } + }, + { + "id": 5, + "type": "PreviewAny", + "pos": [ + 2064.296875, + -2021.0582275390625 + ], + "size": [ + 140, + 76 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 3 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.44", + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [] + }, + { + "id": 6, + "type": "PreviewAny", + "pos": [ + 2060.477294921875, + -1892.009521484375 + ], + "size": [ + 140, + 76 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 4 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.44", + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [] + } + ], + "links": [ + [ + 1, + 2, + 0, + 3, + 0, + "AUDIO" + ], + [ + 3, + 3, + 0, + 5, + 0, + "*" + ], + [ + 4, + 3, + 1, + 6, + 0, + "*" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 1.7088742511245891, + "offset": [ + -1331.3379874532752, + 2114.1241784595736 + ] + }, + "frontendVersion": "1.23.4", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file