Adding Split Fade

This commit is contained in:
dseditor
2025-07-06 01:15:44 +08:00
parent 263f992d04
commit 25eb1ad213
3 changed files with 287 additions and 7 deletions
+1 -1
View File
@@ -524,7 +524,7 @@ NumberListGenerator 節點可根據自訂參數創建數字列表,支援有序
### 功能特色
**分割音檔為清單**:將聲音檔分割為清單,長篇數字人時可分段採樣。
**分割音檔為清單**:將聲音檔分割為清單,長篇數字人時可分段採樣,目前已經更新音檔處理的邏輯,增加FADE功能
**必需輸入:**
- **videofps / 畫格** (Float): 每秒多少格
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 52 KiB

+286 -6
View File
@@ -34,6 +34,10 @@ class AudioListGenerator:
"videofps": ("FLOAT", {"default": 23.976, "min": 1.0, "step": 0.001}),
"samplefps": ("INT", {"default": 81, "min": 1}),
"pad_last_segment": ("BOOLEAN", {"default": True}),
},
"optional": {
"crossfade_duration": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 2.0, "step": 0.01}),
"crossfade_type": (["linear", "cosine", "equal_power"], {"default": "cosine"}),
}
}
@@ -43,27 +47,50 @@ class AudioListGenerator:
FUNCTION = "split"
CATEGORY = "ListHelper"
def split(self, waveform, videofps, samplefps, pad_last_segment):
def split(self, waveform, videofps, samplefps, pad_last_segment, crossfade_duration=0.1, crossfade_type="cosine"):
audio_tensor = waveform["waveform"] # shape: [1, C, N]
sample_rate = waveform["sample_rate"]
total_samples = audio_tensor.shape[-1]
segment_duration_seconds = samplefps / videofps
samples_per_segment = int(segment_duration_seconds * sample_rate)
crossfade_samples = int(crossfade_duration * sample_rate)
audio_list = []
# 確保交叉淡化時間不會超過段落長度的一半
crossfade_samples = min(crossfade_samples, samples_per_segment // 2)
for i in range(0, total_samples, samples_per_segment):
end_idx = min(i + samples_per_segment, total_samples)
segment = audio_tensor[:, :, i:end_idx].clone()
# 計算實際的開始和結束位置,考慮交叉淡化
actual_start = max(0, i - crossfade_samples) if i > 0 else 0
actual_end = min(total_samples, end_idx + crossfade_samples) if end_idx < total_samples else end_idx
# 提取包含交叉淡化部分的音頻段
extended_segment = audio_tensor[:, :, actual_start:actual_end].clone()
# 應用交叉淡化效果
if crossfade_samples > 0:
extended_segment = self._apply_crossfade(
extended_segment,
crossfade_samples,
crossfade_type,
actual_start,
i,
end_idx,
actual_end
)
segment_len = segment.shape[-1]
if pad_last_segment and segment_len < samples_per_segment:
# 如果需要填充最後一個段落
segment_len = extended_segment.shape[-1]
if pad_last_segment and end_idx == total_samples and segment_len < samples_per_segment:
pad_len = samples_per_segment - segment_len
segment = F.pad(segment, (0, pad_len))
extended_segment = F.pad(extended_segment, (0, pad_len))
audio_obj = {
"waveform": segment,
"waveform": extended_segment,
"sample_rate": sample_rate
}
@@ -71,6 +98,85 @@ class AudioListGenerator:
return len(audio_list), audio_list
def _apply_crossfade(self, segment, crossfade_samples, crossfade_type, actual_start, segment_start, segment_end, actual_end):
"""
對音頻段應用交叉淡化效果
Args:
segment: 音頻段張量 [1, C, T]
crossfade_samples: 交叉淡化的樣本數
crossfade_type: 交叉淡化類型
actual_start: 實際開始位置
segment_start: 段落開始位置
segment_end: 段落結束位置
actual_end: 實際結束位置
"""
if crossfade_samples == 0:
return segment
segment_length = segment.shape[-1]
# 創建淡化曲線
fade_curve = self._create_fade_curve(crossfade_samples, crossfade_type)
# 應用淡入效果(段落開始處)
if actual_start < segment_start:
fade_in_length = min(crossfade_samples, segment_length)
fade_in_curve = fade_curve[:fade_in_length]
# 擴展維度以匹配音頻張量 [1, C, fade_in_length]
fade_in_curve = fade_in_curve.unsqueeze(0).unsqueeze(0)
fade_in_curve = fade_in_curve.expand(segment.shape[0], segment.shape[1], -1)
segment[:, :, :fade_in_length] *= fade_in_curve
# 應用淡出效果(段落結束處)
if actual_end > segment_end:
fade_out_length = min(crossfade_samples, segment_length)
fade_out_curve = fade_curve[:fade_out_length].flip(0) # 反轉淡化曲線
# 擴展維度以匹配音頻張量
fade_out_curve = fade_out_curve.unsqueeze(0).unsqueeze(0)
fade_out_curve = fade_out_curve.expand(segment.shape[0], segment.shape[1], -1)
segment[:, :, -fade_out_length:] *= fade_out_curve
return segment
def _create_fade_curve(self, length, fade_type):
"""
創建淡化曲線
Args:
length: 淡化長度(樣本數)
fade_type: 淡化類型 ("linear", "cosine", "equal_power")
Returns:
淡化曲線張量
"""
import torch
import math
if fade_type == "linear":
# 線性淡化:從0到1
curve = torch.linspace(0.0, 1.0, length)
elif fade_type == "cosine":
# 餘弦淡化:更平滑的過渡
t = torch.linspace(0.0, math.pi/2, length)
curve = torch.sin(t)
elif fade_type == "equal_power":
# 等功率淡化:保持總功率恆定
t = torch.linspace(0.0, math.pi/2, length)
curve = torch.sin(t)
else:
# 預設使用線性淡化
curve = torch.linspace(0.0, 1.0, length)
return curve
class AudioToFrameCount:
@classmethod
def INPUT_TYPES(cls):
@@ -812,6 +918,176 @@ class SaveVideoPath:
)
return (full_path,)
class TimestampToLrcNode:
"""
ComfyUI節點:將時間戳格式轉換為LRC歌詞格式
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_text": ("STRING", {
"multiline": True,
"default": ""
}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("lrc_output",)
FUNCTION = "convert_to_lrc"
CATEGORY = "text/processing"
def convert_to_lrc(self, input_text):
"""
將時間戳格式轉換為LRC格式
輸入格式: >> 0:00-0:04\n>> 文本內容
輸出格式: [00:00.00]文本內容
"""
lines = input_text.strip().split('\n')
lrc_lines = []
current_time = None
current_text = ""
for line in lines:
line = line.strip()
# 檢查是否為時間戳行 (格式: >> 0:00-0:04)
time_match = re.match(r'^>>\s*(\d+):(\d+)-(\d+):(\d+)$', line)
if time_match:
# 如果之前有累積的文本,先處理它
if current_time is not None and current_text.strip():
lrc_lines.append(f"[{current_time}]{current_text.strip()}")
# 解析開始時間
start_min = int(time_match.group(1))
start_sec = int(time_match.group(2))
current_time = f"{start_min:02d}:{start_sec:02d}.00"
current_text = ""
# 檢查是否為文本行 (格式: >> 文本內容)
elif line.startswith('>> '):
text_content = line[3:].strip() # 移除 ">> " 前綴
if text_content: # 只添加非空文本
if current_text:
current_text += " " + text_content
else:
current_text = text_content
# 處理空行或其他格式
elif line == '' or line == '>>':
# 空行保持當前狀態,不做處理
continue
else:
# 其他格式的行,嘗試作為文本處理
if line and current_time is not None:
if current_text:
current_text += " " + line
else:
current_text = line
# 處理最後一段文本
if current_time is not None and current_text.strip():
lrc_lines.append(f"[{current_time}]{current_text.strip()}")
# 合併結果
lrc_output = '\n'.join(lrc_lines)
return (lrc_output,)
try:
import opencc
except ImportError:
print("請安裝opencc庫: pip install opencc-python-reimplemented")
opencc = None
class ChineseConverterNode:
"""
ComfyUI節點:中文簡繁轉換
使用opencc庫進行高質量轉換
布林開關控制:True=簡體轉繁體,False=繁體轉簡體
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_text": ("STRING", {
"multiline": True,
"default": ""
}),
"simp_to_trad": ("BOOLEAN", {
"default": True,
"label_on": "簡體→繁體",
"label_off": "繁體→簡體"
}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("converted_text",)
FUNCTION = "convert_chinese"
CATEGORY = "text/processing"
def __init__(self):
"""初始化轉換器"""
if opencc is None:
self.s2t_converter = None
self.t2s_converter = None
print("錯誤:opencc庫未安裝,請執行: pip install opencc-python-reimplemented")
else:
try:
# 簡體轉繁體轉換器
self.s2t_converter = opencc.OpenCC('s2t.json')
# 繁體轉簡體轉換器
self.t2s_converter = opencc.OpenCC('t2s.json')
except Exception as e:
print(f"opencc初始化失敗: {e}")
self.s2t_converter = None
self.t2s_converter = None
def convert_chinese(self, input_text, simp_to_trad):
"""
轉換中文文本
Args:
input_text: 輸入文本
simp_to_trad: True=簡體轉繁體,False=繁體轉簡體
Returns:
轉換後的文本
"""
if not input_text.strip():
return ("",)
# 檢查opencc是否可用
if opencc is None:
error_msg = "錯誤:請先安裝opencc庫\n執行命令: pip install opencc-python-reimplemented"
print(error_msg)
return (error_msg,)
try:
if simp_to_trad:
# 簡體轉繁體
if self.s2t_converter is None:
self.s2t_converter = opencc.OpenCC('s2t.json')
converted_text = self.s2t_converter.convert(input_text)
else:
# 繁體轉簡體
if self.t2s_converter is None:
self.t2s_converter = opencc.OpenCC('t2s.json')
converted_text = self.t2s_converter.convert(input_text)
return (converted_text,)
except Exception as e:
error_msg = f"轉換失敗: {str(e)}"
print(error_msg)
return (error_msg,)
@@ -825,6 +1101,8 @@ NODE_CLASS_MAPPINGS = {
"CeilDivide": CeilDivide,
"LoadVideoPath": LoadVideoPath,
"SaveVideoPath": SaveVideoPath,
"TimestampToLrcNode": TimestampToLrcNode,
"ChineseConverterNode": ChineseConverterNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -837,5 +1115,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"CeilDivide": "CeilDivide",
"LoadVideoPath": "LoadVideoPath",
"SaveVideoPath": "SaveVideoPath",
"TimestampToLrcNode": "TimestampToLrcNode",
"ChineseConverterNode": "ChineseConverterNode",
}