feat: add TalkObjectInput and TalkObjectsBuilder classes for managing audio and mask inputs; enhance LightX2VConfigCombiner to support multi-person talk objects configuration

This commit is contained in:
gaclove
2025-09-24 18:52:58 +08:00
parent 87b7e429c4
commit 698030281c
+182
View File
@@ -553,6 +553,96 @@ class LightX2VLoRALoader:
return (lora_chain,)
class TalkObjectInput:
"""单个谈话对象(音频+遮罩)输入节点"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio_source": (["upload", "url"], {"default": "upload", "tooltip": "音频输入方式"}),
"mask_source": (["upload", "url", "none"], {"default": "upload", "tooltip": "遮罩输入方式"}),
},
"optional": {
"audio": ("AUDIO", {"tooltip": "上传的音频文件"}),
"audio_url": ("STRING", {"default": "", "tooltip": "音频文件URL路径"}),
"mask": ("MASK", {"tooltip": "上传的遮罩图像"}),
"mask_url": ("STRING", {"default": "", "tooltip": "遮罩图像URL路径"}),
},
}
RETURN_TYPES = ("TALK_OBJECT",)
RETURN_NAMES = ("talk_object",)
FUNCTION = "create_talk_object"
CATEGORY = "LightX2V/Audio"
def create_talk_object(self, audio_source, mask_source, audio=None, audio_url="", mask=None, mask_url=""):
"""创建单个谈话对象配置"""
talk_object = {}
# 处理音频
if audio_source == "upload" and audio is not None:
# 音频将在推理时转换为临时文件
talk_object["audio_data"] = audio
talk_object["audio_type"] = "upload"
elif audio_source == "url" and audio_url:
talk_object["audio"] = audio_url
talk_object["audio_type"] = "url"
else:
return (None,) # 无效的音频输入
# 处理遮罩
if mask_source == "upload" and mask is not None:
talk_object["mask_data"] = mask
talk_object["mask_type"] = "upload"
elif mask_source == "url" and mask_url:
talk_object["mask"] = mask_url
talk_object["mask_type"] = "url"
elif mask_source == "none":
# 不使用遮罩
talk_object["mask"] = None
talk_object["mask_type"] = "none"
return (talk_object,)
class TalkObjectsBuilder:
"""构建多人对话配置的节点"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"number": ("INT", {"default": 1, "min": 0, "max": 10, "tooltip": "谈话对象数量,0表示无,1表示单人,2+表示多人"}),
},
"optional": {
# 支持最多10个谈话对象
**{f"talk_object_{i}": ("TALK_OBJECT",) for i in range(1, 11)}
},
}
RETURN_TYPES = ("TALK_OBJECTS_CONFIG",)
RETURN_NAMES = ("talk_objects_config",)
FUNCTION = "build_talk_objects"
CATEGORY = "LightX2V/Audio"
def build_talk_objects(self, number, **kwargs):
"""构建谈话对象列表"""
if number == 0:
return (None,)
talk_objects = []
for i in range(1, number + 1):
talk_obj = kwargs.get(f"talk_object_{i}")
if talk_obj is not None:
talk_objects.append(talk_obj)
if not talk_objects:
return (None,)
return ({"talk_objects": talk_objects},)
class LightX2VConfigCombiner:
def __init__(self):
self.config_manager = ModularConfigManager()
@@ -580,6 +670,7 @@ class LightX2VConfigCombiner:
{"tooltip": "Memory optimization configuration"},
),
"lora_chain": ("LORA_CHAIN", {"tooltip": "LoRA chain configuration"}),
"talk_objects_config": ("TALK_OBJECTS_CONFIG", {"tooltip": "Multi-person talk objects configuration"}),
},
}
@@ -595,6 +686,7 @@ class LightX2VConfigCombiner:
quantization_config=None,
memory_config=None,
lora_chain=None,
talk_objects_config=None,
):
configs = {
"inference": inference_config,
@@ -611,6 +703,9 @@ class LightX2VConfigCombiner:
if lora_chain:
config.lora_configs = lora_chain
if talk_objects_config:
config.talk_objects_config = talk_objects_config
logging.info("lightx2v config: " + json.dumps(config, indent=2, ensure_ascii=False))
return (config,)
@@ -771,6 +866,89 @@ class LightX2VModularInference:
logging.info(f"Audio saved to {tmp.name}")
# 处理多人对话对象
if hasattr(config, "talk_objects_config") and config.talk_objects_config:
talk_objects = config.talk_objects_config.get("talk_objects", [])
processed_talk_objects = []
for talk_obj in talk_objects:
processed_obj = {}
# 处理音频
if talk_obj.get("audio_type") == "upload" and "audio_data" in talk_obj:
audio_data = talk_obj["audio_data"]
# 处理音频数据,转换为临时文件
if isinstance(audio_data, dict) and "waveform" in audio_data and "sample_rate" in audio_data:
waveform = audio_data["waveform"]
sample_rate = audio_data["sample_rate"]
if isinstance(waveform, torch.Tensor):
if waveform.dim() == 3:
waveform = waveform[0]
if waveform.dim() == 2:
waveform = waveform.transpose(0, 1)
waveform = waveform.cpu().numpy()
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
try:
import scipy.io.wavfile as wavfile
if waveform.ndim == 1:
wavfile.write(tmp.name, sample_rate, waveform)
else:
if waveform.shape[0] < waveform.shape[1]:
waveform = waveform.T
wavfile.write(tmp.name, sample_rate, waveform)
except ImportError:
import wave
with wave.open(tmp.name, "wb") as wav_file:
wav_file.setnchannels(1 if waveform.ndim == 1 else waveform.shape[-1])
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate)
if waveform.dtype != np.int16:
waveform = (waveform * 32767).astype(np.int16)
wav_file.writeframes(waveform.tobytes())
processed_obj["audio"] = tmp.name
temp_files.append(tmp.name)
logging.info(f"Talk object audio saved to {tmp.name}")
elif talk_obj.get("audio_type") == "url":
processed_obj["audio"] = talk_obj.get("audio")
# 处理遮罩
if talk_obj.get("mask_type") == "upload" and "mask_data" in talk_obj:
mask_data = talk_obj["mask_data"]
# 假设 mask_data 是一个 tensor 或 numpy array
if isinstance(mask_data, torch.Tensor):
mask_np = (mask_data[0].cpu().numpy() * 255).astype(np.uint8)
elif isinstance(mask_data, np.ndarray):
mask_np = (mask_data * 255).astype(np.uint8)
else:
mask_np = mask_data
mask_image = Image.fromarray(mask_np)
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
mask_image.save(tmp.name)
processed_obj["mask"] = tmp.name
temp_files.append(tmp.name)
logging.info(f"Talk object mask saved to {tmp.name}")
elif talk_obj.get("mask_type") == "url":
processed_obj["mask"] = talk_obj.get("mask")
elif talk_obj.get("mask_type") == "none":
processed_obj["mask"] = None
if "audio" in processed_obj:
processed_talk_objects.append(processed_obj)
# 设置 talk_objects 到 config
if processed_talk_objects:
config.talk_objects = processed_talk_objects
logging.info(f"Processed {len(processed_talk_objects)} talk objects")
config_hash = self._get_config_hash(config)
needs_reinit = (
self.__class__._current_runner is None or self.__class__._current_config_hash != config_hash or getattr(config, "lazy_load", False)
@@ -833,6 +1011,8 @@ NODE_CLASS_MAPPINGS = {
"LightX2VLoRALoader": LightX2VLoRALoader,
"LightX2VConfigCombiner": LightX2VConfigCombiner,
"LightX2VModularInference": LightX2VModularInference,
"TalkObjectInput": TalkObjectInput,
"TalkObjectsBuilder": TalkObjectsBuilder,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -843,4 +1023,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"LightX2VLoRALoader": "LightX2V LoRA Loader",
"LightX2VConfigCombiner": "LightX2V Config Combiner",
"LightX2VModularInference": "LightX2V Modular Inference",
"TalkObjectInput": "Talk Object Input",
"TalkObjectsBuilder": "Talk Objects Builder",
}