The eff537d commit changed all files from 755 to 644.
Restores original executable permissions.
205 lines
8.7 KiB
Python
Executable File
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)"
|
|
}
|