Files
havvk-ComfyUI_AIIA/aiia_audio_processor.py
T
Hawk Lee e21a5c9eba fix: restore file permissions to 755 (reverts eff537d mode change)
The eff537d commit changed all files from 755 to 644.
Restores original executable permissions.
2026-02-16 12:15:32 +08:00

205 lines
8.7 KiB
Python
Executable File

import torch
import numpy as np
class AIIA_Audio_Silence_Splitter:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio": ("AUDIO",),
"max_duration": ("FLOAT", {"default": 27.0, "min": 5.0, "max": 29.0, "step": 0.1}),
"silence_threshold": ("FLOAT", {"default": 0.001, "min": 0.0001, "max": 0.1, "step": 0.0001}),
"min_silence_duration": ("FLOAT", {"default": 0.3, "min": 0.05, "max": 2.0, "step": 0.05}),
}
}
RETURN_TYPES = ("WHISPER_CHUNKS", "INT")
RETURN_NAMES = ("whisper_chunks", "chunk_count")
FUNCTION = "split_audio"
CATEGORY = "AIIA/audio"
def split_audio(self, audio, max_duration, silence_threshold, min_silence_duration):
waveform = audio["waveform"]
sample_rate = audio["sample_rate"]
# 强制在 CPU 处理以防 OOM
waveform_cpu = waveform.cpu()
# 计算能量
# waveform shape: [B, C, T]
energy = torch.abs(waveform_cpu).mean(dim=1).squeeze()
if energy.ndim > 1: energy = energy.mean(dim=0)
# 1. 识别静音区间
is_silence = energy < silence_threshold
sil_np = is_silence.numpy()
# 寻找变化点
diff = np.diff(sil_np.astype(int))
starts = np.where(diff == 1)[0]
ends = np.where(diff == -1)[0]
if sil_np[0]: starts = np.insert(starts, 0, 0)
if sil_np[-1]: ends = np.append(ends, len(sil_np) - 1)
# 保证成对
if len(starts) > len(ends): starts = starts[:len(ends)]
elif len(ends) > len(starts): ends = ends[:len(starts)]
# 过滤过短的静音
# 修正变量名:使用参数传入的 min_silence_duration
min_sil_samples = int(min_silence_duration * sample_rate)
valid_silence_gaps = []
for s, e in zip(starts, ends):
if e - s >= min_sil_samples:
valid_silence_gaps.append((s / sample_rate, e / sample_rate))
# 2. 贪心构建
total_duration = waveform_cpu.shape[-1] / sample_rate
chunks = []
current_start = 0.0
while current_start < total_duration:
target_end = current_start + max_duration
if target_end >= total_duration:
chunks.append([current_start, total_duration])
break
best_gap_mid = -1
for g_start, g_end in valid_silence_gaps:
g_mid = (g_start + g_end) / 2
if current_start < g_mid <= target_end:
best_gap_mid = g_mid
elif g_mid > target_end:
break
if best_gap_mid != -1:
chunks.append([current_start, best_gap_mid])
current_start = best_gap_mid
else:
chunks.append([current_start, target_end])
current_start = target_end
whisper_chunks_data = {
"text": "",
"chunks": [
{"timestamp": [round(c[0], 3), round(c[1], 3)], "text": f"Chunk {i}", "speaker": "AIIA_SMART_CHUNK"}
for i, c in enumerate(chunks)
],
"language": ""
}
return (whisper_chunks_data, len(chunks))
class AIIA_Audio_PostProcess:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio": ("AUDIO",),
"target_rate": (["44100", "48000", "24000", "22050", "Original"], {"default": "44100"}),
"fade_length": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 5.0, "step": 0.1, "tooltip": "Fade in/out duration in seconds"}),
"normalize": ("BOOLEAN", {"default": True, "tooltip": "Normalize to -1dB"}),
"resampling_alg": (["sinc_interp_hann", "sinc_interp_kaiser"], {"default": "sinc_interp_hann"}),
"lowpass_cutoff": ("INT", {"default": 11000, "min": 0, "max": 24000, "step": 100, "tooltip": "Apply LowPass filter at this frequency (Hz) to remove aliasing noise. 0 to disable."}),
"highpass_cutoff": ("INT", {"default": 60, "min": 0, "max": 2000, "step": 10, "tooltip": "Apply HighPass filter at this frequency (Hz) to remove low-end rumble/DC offset. 0 to disable."}),
},
"optional": {
"splice_info": ("SPLICE_INFO",),
}
}
RETURN_TYPES = ("AUDIO", "SPLICE_INFO")
RETURN_NAMES = ("audio", "splice_info")
FUNCTION = "process_audio"
CATEGORY = "AIIA/Audio"
def process_audio(self, audio, target_rate, fade_length, normalize, resampling_alg, lowpass_cutoff, highpass_cutoff, splice_info=None):
import torchaudio
import copy
import torchaudio.functional as F
waveform = audio["waveform"] # [B, C, T] usually
original_rate = audio["sample_rate"]
if waveform.ndim == 2:
waveform = waveform.unsqueeze(0)
new_splice_info = copy.deepcopy(splice_info) if splice_info else None
# 1. Resample
if target_rate != "Original":
new_rate = int(target_rate)
if new_rate != original_rate:
# Map legacy/UI friendly names to valid torchaudio methods if necessary
valid_method = resampling_alg
if resampling_alg in ["sinc_best", "sinc_fast"]: valid_method = "sinc_interp_hann"
elif resampling_alg == "kaiser_best": valid_method = "sinc_interp_kaiser"
resampler = torchaudio.transforms.Resample(
orig_freq=original_rate,
new_freq=new_rate,
resampling_method=valid_method,
dtype=waveform.dtype
)
waveform = resampler(waveform)
# Update splice info if present
if new_splice_info and "splice_points" in new_splice_info:
scale_factor = new_rate / original_rate
new_splice_info["splice_points"] = [int(p * scale_factor) for p in new_splice_info["splice_points"]]
if "total_samples" in new_splice_info:
new_splice_info["total_samples"] = int(new_splice_info["total_samples"] * scale_factor)
original_rate = new_rate
# 2. HighPass Filter (Remove Rumble / DC Offset)
if highpass_cutoff > 0:
# Single pass (2nd order, 12dB/oct) is safer for preserving voice body.
# Cascading here was too aggressive and killed the bass details.
waveform = F.highpass_biquad(waveform, original_rate, cutoff_freq=highpass_cutoff)
# 3. LowPass Filter (Anti-Aliasing / Denoise)
if lowpass_cutoff > 0:
# lowpass_biquad is a 2nd order filter (12dB/octave).
# To remove stubborn aliasing ("stripes"), we need a steeper slope.
# We cascade it 6 times to create a ~72dB/octave "Brick Wall" effect.
for _ in range(6):
waveform = F.lowpass_biquad(waveform, original_rate, cutoff_freq=lowpass_cutoff)
# 3. Fade In/Out
if fade_length > 0:
fade_samples = int(fade_length * original_rate)
if fade_samples * 2 < waveform.shape[-1]:
fade_in = torch.linspace(0, 1, fade_samples, device=waveform.device)
fade_out = torch.linspace(1, 0, fade_samples, device=waveform.device)
# Apply to last dim (time)
# Ensure we handle multiple channels correctly
if waveform.ndim == 3:
waveform[:, :, :fade_samples] *= fade_in
waveform[:, :, -fade_samples:] *= fade_out
else:
waveform[..., :fade_samples] *= fade_in
waveform[..., -fade_samples:] *= fade_out
# 3. Normalize (to -1dB = 0.891)
if normalize:
peak = waveform.abs().max()
# Safe Normalize: Only normalize if signal is above -30dB (0.03)
# This prevents boosting pure silence/noise floors into loud artifacts.
if peak > 0.03:
target_peak = 0.891 # -1.0 dB
waveform = waveform * (target_peak / peak)
return ({"waveform": waveform, "sample_rate": original_rate}, new_splice_info)
NODE_CLASS_MAPPINGS = {
"AIIA_Audio_Silence_Splitter": AIIA_Audio_Silence_Splitter,
"AIIA_Audio_PostProcess": AIIA_Audio_PostProcess
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AIIA_Audio_Silence_Splitter": "Audio Smart Chunker (Silence-based)",
"AIIA_Audio_PostProcess": "Audio Post-Process (Resample/Fade/Norm)"
}