Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10c9eff16f | ||
|
|
edd7af986d | ||
|
|
1dc31927e3 | ||
|
|
36ef7d25ef | ||
|
|
b766b8b65d | ||
|
|
6579ff20b4 | ||
|
|
2fbee59c3e | ||
|
|
d3aaa19148 | ||
|
|
e32a3675fc | ||
|
|
b72e7dda08 | ||
|
|
0f77f28a95 | ||
|
|
289f83675b | ||
|
|
36633b4c72 | ||
|
|
4f45457811 | ||
|
|
c39890cd64 | ||
|
|
90f1e49263 | ||
|
|
9a1cf205db | ||
|
|
45eacb6a50 | ||
|
|
6cb2b57463 | ||
|
|
59f654fa39 | ||
|
|
be8ccc1dc4 | ||
|
|
228e5d9183 | ||
|
|
8afe6d0383 | ||
|
|
5f7190b08f | ||
|
|
a70a9b4bb1 | ||
|
|
b796e66890 | ||
|
|
f1a663779a | ||
|
|
b0aa972326 | ||
|
|
ef927a7ed1 | ||
|
|
aa8fc59051 | ||
|
|
ce62204392 | ||
|
|
837f28142d | ||
|
|
60c79c991d | ||
|
|
078aaeb679 | ||
|
|
d9edbd535e | ||
|
|
b4a61b21c3 | ||
|
|
bdc4193ffe |
@@ -10,6 +10,12 @@ For business cooperation, please contact email 389570357@qq.com
|
||||
|
||||
##### `最新`:
|
||||
|
||||
- 新增 SenseVoice
|
||||
|
||||
- [新增JS-SDK,方便直接在前端项目中使用comfyui](https://github.com/shadowcz007/comfyui-js-sdk)
|
||||
|
||||
- 新增API调用图像生成节点 TextToImage Siliconflow,可以直接调用Siliconflow提供的flux生成图像
|
||||
|
||||
- [增加 Her 的DEMO页面,和数字人对话](https://github.com/shadowcz007/ComfyUI-Backend-MixlabNodes/blob/main/workflow/her_demo_workflow.json)
|
||||
|
||||
- 右键菜单支持 text-to-text,方便对 prompt 词补全,支持云LLM或者是本地LLM。
|
||||
|
||||
+24
-2
@@ -1006,8 +1006,8 @@ from .nodes.ImageNode import DepthViewer_,ImageBatchToList_,ImageListToBatch_,Co
|
||||
# from .nodes.Vae import VAELoader,VAEDecode
|
||||
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
|
||||
|
||||
from .nodes.Audio import AudioPlayNode,SpeechRecognition,SpeechSynthesis
|
||||
from .nodes.Utils import KeyInput,IncrementingListNode,ListSplit,CreateLoraNames,CreateSampler_names,CreateCkptNames,CreateSeedNode,TESTNODE_,TESTNODE_TOKEN,AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,MultiplicationNode
|
||||
from .nodes.Audio import AudioPlayNode,SpeechRecognition,SpeechSynthesis,AnalyzeAudioNone
|
||||
from .nodes.Utils import CreateJsonNode,KeyInput,IncrementingListNode,ListSplit,CreateLoraNames,CreateSampler_names,CreateCkptNames,CreateSeedNode,TESTNODE_,TESTNODE_TOKEN,AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,MultiplicationNode
|
||||
from .nodes.Mask import PreviewMask_,MaskListReplace,MaskListMerge,OutlineMask,FeatheredMask
|
||||
|
||||
from .nodes.Style import ApplyVisualStylePrompting,StyleAlignedReferenceSampler,StyleAlignedBatchAlign,StyleAlignedSampleReferenceLatents
|
||||
@@ -1045,6 +1045,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"SaveImageToLocal":SaveImageToLocal,
|
||||
"SaveImageAndMetadata_":SaveImageAndMetadata,
|
||||
"ComparingTwoFrames_":ComparingTwoFrames,
|
||||
"CreateJsonNode":CreateJsonNode,
|
||||
|
||||
# Image
|
||||
"MirroredImage":MirroredImage,
|
||||
@@ -1102,6 +1103,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"SpeechRecognition":SpeechRecognition,
|
||||
"SpeechSynthesis":SpeechSynthesis,
|
||||
"AudioPlay":AudioPlayNode,
|
||||
"AnalyzeAudio":AnalyzeAudioNone,
|
||||
|
||||
# Text
|
||||
"TextToNumber":TextToNumber,
|
||||
@@ -1219,12 +1221,14 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SpeechSynthesis":"SpeechSynthesis ♾️Mixlab",
|
||||
"SpeechRecognition":"SpeechRecognition ♾️Mixlab",
|
||||
"AudioPlay":"Preview Audio ♾️Mixlab",
|
||||
"AnalyzeAudio":"Analyze Audio ♾️Mixlab",
|
||||
|
||||
# Utils
|
||||
"DynamicDelayProcessor":"DynamicDelayByText ♾️Mixlab",
|
||||
"MultiplicationNode":"Math Operation ♾️Mixlab",
|
||||
"ListSplit_":"Split List ♾️Mixlab",
|
||||
"SwitchByIndex":"List Switch By Index ♾️Mixlab",
|
||||
"CreateJsonNode":"Create Json",
|
||||
|
||||
# "GamePal":"GamePal ♾️Mixlab",
|
||||
# Experiment
|
||||
@@ -1427,6 +1431,24 @@ try:
|
||||
except Exception as e:
|
||||
logging.info('FishSpeech.available False' )
|
||||
|
||||
try:
|
||||
from .nodes.SenseVoice import SenseVoiceNode
|
||||
logging.info('SenseVoice.available')
|
||||
NODE_CLASS_MAPPINGS['SenseVoiceNode']=SenseVoiceNode
|
||||
NODE_DISPLAY_NAME_MAPPINGS["SenseVoiceNode"]= "Sense Voice ♾️Mixlab"
|
||||
|
||||
except Exception as e:
|
||||
logging.info('SenseVoice.available False' )
|
||||
|
||||
try:
|
||||
from .nodes.Whisper import LoadWhisperModel,WhisperTranscribe
|
||||
logging.info('Whisper.available')
|
||||
NODE_CLASS_MAPPINGS['LoadWhisperModel_']=LoadWhisperModel
|
||||
NODE_CLASS_MAPPINGS['WhisperTranscribe_']=WhisperTranscribe
|
||||
NODE_DISPLAY_NAME_MAPPINGS["LoadWhisperModel_"]= "Load Whisper Model ♾️Mixlab"
|
||||
NODE_DISPLAY_NAME_MAPPINGS["WhisperTranscribe_"]= "Whisper Transcribe ♾️Mixlab"
|
||||
|
||||
except Exception as e:
|
||||
logging.info('Whisper.available False' )
|
||||
|
||||
logging.info('\033[93m -------------- \033[0m')
|
||||
|
||||
+1862
-305
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,101 @@ import os
|
||||
import folder_paths
|
||||
import torchaudio
|
||||
|
||||
class AnyType(str):
|
||||
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
|
||||
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
any_type = AnyType("*")
|
||||
|
||||
|
||||
def analyze_audio_data(audio_data):
|
||||
total_duration = 0
|
||||
total_gap_duration = 0
|
||||
emotion_counts = {}
|
||||
audio_types = set()
|
||||
languages = set()
|
||||
|
||||
for i, entry in enumerate(audio_data):
|
||||
# Calculate the duration of each audio segment
|
||||
start_time = entry['start_time']
|
||||
end_time = entry['end_time']
|
||||
duration = end_time - start_time
|
||||
total_duration += duration
|
||||
|
||||
# Count the emotions
|
||||
if "emotion" in entry:
|
||||
emotion = entry['emotion']
|
||||
if emotion in emotion_counts:
|
||||
emotion_counts[emotion] += 1
|
||||
else:
|
||||
emotion_counts[emotion] = 1
|
||||
|
||||
# Collect the audio types
|
||||
if "audio_type" in entry:
|
||||
audio_types.add(entry['audio_type'])
|
||||
|
||||
if "language" in entry:
|
||||
languages.add(entry['language'])
|
||||
|
||||
# Calculate gap duration if not the last entry
|
||||
if i < len(audio_data) - 1:
|
||||
next_start_time = audio_data[i + 1]['start_time']
|
||||
gap_duration = next_start_time - end_time
|
||||
if gap_duration > 0:
|
||||
total_gap_duration += gap_duration
|
||||
|
||||
# Get the most frequent emotion
|
||||
if len(emotion_counts.keys())>0:
|
||||
most_frequent_emotion = max(emotion_counts, key=emotion_counts.get)
|
||||
else:
|
||||
most_frequent_emotion=None
|
||||
|
||||
# Convert audio_types set to list for better readability
|
||||
audio_types = list(audio_types)
|
||||
|
||||
languages=list(languages)
|
||||
|
||||
# Print the results
|
||||
print(f"Total Effective Duration: {total_duration:.2f} seconds")
|
||||
print(f"Total Gap Duration: {total_gap_duration:.2f} seconds")
|
||||
print(f"Emotion Changes: {emotion_counts}")
|
||||
print(f"Most Frequent Emotion: {most_frequent_emotion}")
|
||||
print(f"Audio Types: {audio_types}")
|
||||
|
||||
|
||||
return {
|
||||
"total_duration": total_duration,
|
||||
"total_gap_duration": total_gap_duration,
|
||||
"emotion_changes": emotion_counts,
|
||||
"most_frequent_emotion": most_frequent_emotion,
|
||||
"audio_types": audio_types,
|
||||
"languages":languages
|
||||
}
|
||||
|
||||
|
||||
# 分析音频数据
|
||||
class AnalyzeAudioNone:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"json":(any_type,),},
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type,)
|
||||
RETURN_NAMES = ("result",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/Audio"
|
||||
|
||||
def run(self,json):
|
||||
result=analyze_audio_data(json)
|
||||
return (result,)
|
||||
|
||||
|
||||
|
||||
class SpeechRecognition:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
|
||||
+26
-5
@@ -460,7 +460,8 @@ class SiliconflowFreeNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
model_list= [
|
||||
"Qwen/Qwen2-7B-Instruct",
|
||||
"Qwen/Qwen2.5-7B-Instruct",
|
||||
"Qwen/Qwen2-7B-Instruct",
|
||||
"THUDM/glm-4-9b-chat",
|
||||
"01-ai/Yi-1.5-9B-Chat-16K",
|
||||
"meta-llama/Meta-Llama-3.1-8B-Instruct"
|
||||
@@ -785,9 +786,12 @@ class JsonRepair:
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"json_string":("STRING", {"forceInput": True,}),
|
||||
"key":("STRING", {"multiline": False,"dynamicPrompts": False,"default": ""}),
|
||||
}
|
||||
"json_string":("STRING", {"forceInput": True,}),
|
||||
"key":("STRING", {"multiline": False,"dynamicPrompts": False,"default": ""}),
|
||||
},
|
||||
"optional":{
|
||||
"json_string2":("STRING", {"forceInput": True,})
|
||||
},
|
||||
}
|
||||
|
||||
INPUT_IS_LIST = False
|
||||
@@ -799,8 +803,11 @@ class JsonRepair:
|
||||
|
||||
CATEGORY = "♾️Mixlab/GPT"
|
||||
|
||||
def run(self, json_string,key=""):
|
||||
def run(self, json_string,key="",json_string2=None):
|
||||
|
||||
if not isinstance(json_string, str):
|
||||
json_string=json.dumps(json_string)
|
||||
|
||||
json_string=extract_json_strings(json_string)
|
||||
# print(json_string)
|
||||
good_json_string = repair_json(json_string)
|
||||
@@ -808,6 +815,20 @@ class JsonRepair:
|
||||
# 将 JSON 字符串解析为 Python 对象
|
||||
data = json.loads(good_json_string)
|
||||
|
||||
if json_string2!=None:
|
||||
if not isinstance(json_string2, str):
|
||||
json_string2=json.dumps(json_string2)
|
||||
|
||||
json_string2=extract_json_strings(json_string2)
|
||||
# print(json_string)
|
||||
good_json_string2 = repair_json(json_string2)
|
||||
|
||||
# 将 JSON 字符串解析为 Python 对象
|
||||
data2 = json.loads(good_json_string2)
|
||||
|
||||
data={**data, **data2}
|
||||
|
||||
|
||||
v=""
|
||||
if key!="" and (key in data):
|
||||
v=data[key]
|
||||
|
||||
+30
-13
@@ -13,12 +13,26 @@ import json,io
|
||||
import comfy.utils
|
||||
from comfy.cli_args import args
|
||||
import cv2
|
||||
import string
|
||||
import string,re
|
||||
import math,glob
|
||||
from .Watcher import FolderWatcher
|
||||
|
||||
from itertools import product
|
||||
|
||||
|
||||
# 文件名排序
|
||||
def sort_by_filename(items):
|
||||
def extract_parts(filename):
|
||||
# 使用正则表达式将文件名拆分为数字和非数字部分
|
||||
parts = re.split(r'(\d+)', filename)
|
||||
# 将数字部分转换为整数以便正确排序,同时保留非数字部分
|
||||
parts = [int(part) if part.isdigit() else part for part in parts]
|
||||
return parts
|
||||
|
||||
# 按照 file_name 的拆分部分进行排序
|
||||
sorted_items = sorted(items, key=lambda x: extract_parts(x['file_name']))
|
||||
return sorted_items
|
||||
|
||||
# 将PIL图片转换为OpenCV格式
|
||||
def pil_to_opencv(image):
|
||||
open_cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
|
||||
@@ -768,8 +782,7 @@ def areaToMask(x,y,w,h,image):
|
||||
# return bg_image
|
||||
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ps的正片叠底
|
||||
# 可以基于https://www.cnblogs.com/jsxyhelu/p/16947810.html ,用gpt写python代码
|
||||
@@ -1433,7 +1446,7 @@ class LoadImagesFromPath:
|
||||
},
|
||||
"optional":{
|
||||
"white_bg": (["disable","enable"],),
|
||||
"newest_files": (["enable", "disable"],),
|
||||
"sort_by": (["file_name", "newest"],),#根据文件名来排序,还是按照最新创建时间
|
||||
"index_variable":("INT", {
|
||||
"default": 0,
|
||||
"min": -1, #Minimum value
|
||||
@@ -1462,7 +1475,7 @@ class LoadImagesFromPath:
|
||||
watcher_folder=None
|
||||
|
||||
# 运行的函数
|
||||
def run(self,file_path,white_bg,newest_files,index_variable,watcher,result,prompt,seed=1):
|
||||
def run(self,file_path,white_bg,sort_by,index_variable,watcher,result,prompt,seed=1):
|
||||
global watcher_folder
|
||||
# print('###监听:',watcher_folder,watcher,file_path,result)
|
||||
|
||||
@@ -1485,19 +1498,23 @@ class LoadImagesFromPath:
|
||||
# 当开启了监听,则取最新的,第一个文件
|
||||
if watcher=='enable':
|
||||
index_variable=0
|
||||
newest_files='enable'
|
||||
sort_by='newest'
|
||||
|
||||
# 排序
|
||||
sorted_files = sorted(images, key=lambda x: os.path.getmtime(x['file_path']), reverse=(newest_files=='enable'))
|
||||
if sort_by=='newest':
|
||||
sorted_files = sorted(images, key=lambda x: os.path.getmtime(x['file_path']), reverse=True)
|
||||
elif sort_by=='file_name':
|
||||
# 根据文件名排序
|
||||
sorted_files = sort_by_filename(images)
|
||||
|
||||
imgs=[]
|
||||
masks=[]
|
||||
file_names=[]
|
||||
file_paths=[]
|
||||
|
||||
for im in sorted_files:
|
||||
imgs.append(im['image'])
|
||||
masks.append(im['mask'])
|
||||
file_names.append(im['file_name'])
|
||||
file_paths.append(im['file_path'])
|
||||
|
||||
# print('index_variable',index_variable)
|
||||
|
||||
@@ -1505,13 +1522,13 @@ class LoadImagesFromPath:
|
||||
if index_variable!=-1:
|
||||
imgs=[imgs[index_variable]] if index_variable < len(imgs) else None
|
||||
masks=[masks[index_variable]] if index_variable < len(masks) else None
|
||||
file_names=[file_names[index_variable]] if index_variable < len(file_names) else None
|
||||
file_paths=[file_paths[index_variable]] if index_variable < len(file_paths) else None
|
||||
except Exception as e:
|
||||
print("发生了一个未知的错误:", str(e))
|
||||
|
||||
# print('#prompt::::',prompt)
|
||||
# return {"ui": {"seed": [1]}, "result":(imgs,masks,prompt,file_names,)}
|
||||
return (imgs,masks,prompt,file_names,)
|
||||
return (imgs,masks,prompt,file_paths,)
|
||||
|
||||
|
||||
# TODO 扩大选区的功能,重新输出mask
|
||||
@@ -2873,14 +2890,14 @@ class ResizeImage:
|
||||
"default": 512,
|
||||
"min": 1, #Minimum value
|
||||
"max": 8192, #Maximum value
|
||||
"step": 8, #Slider's step
|
||||
"step": 1, #Slider's step
|
||||
"display": "number" # Cosmetic only: display as "number" or "slider"
|
||||
}),
|
||||
"height": ("INT",{
|
||||
"default": 512,
|
||||
"min": 1, #Minimum value
|
||||
"max": 8192, #Maximum value
|
||||
"step": 8, #Slider's step
|
||||
"step": 1, #Slider's step
|
||||
"display": "number" # Cosmetic only: display as "number" or "slider"
|
||||
}),
|
||||
"scale_option": (["width","height",'overall','center'],),
|
||||
|
||||
+32
-2
@@ -1,4 +1,5 @@
|
||||
# Referenced some code:https://github.com/IuvenisSapiens/ComfyUI_MiniCPM-V-2_6-int4
|
||||
# https://github.com/CY-CHENYUE/ComfyUI-MiniCPM-Plus
|
||||
|
||||
import os
|
||||
import torch
|
||||
@@ -35,6 +36,7 @@ class MiniCPM_VQA_Simple:
|
||||
"images": ("IMAGE",),
|
||||
"text": ("STRING", {"default": "", "multiline": True}),
|
||||
"seed": ("INT", {"default": -1}), # add seed parameter, default is -1
|
||||
"extract_keywords":("BOOLEAN", {"default": False}),
|
||||
"temperature": (
|
||||
"FLOAT",
|
||||
{
|
||||
@@ -46,7 +48,9 @@ class MiniCPM_VQA_Simple:
|
||||
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_TYPES = ("STRING","STRING",)
|
||||
RETURN_NAMES = ("result","keywords",)
|
||||
|
||||
FUNCTION = "inference"
|
||||
CATEGORY = "♾️Mixlab/Image"
|
||||
|
||||
@@ -55,6 +59,7 @@ class MiniCPM_VQA_Simple:
|
||||
images,
|
||||
text,
|
||||
seed, # add seed parameter, default is -1
|
||||
extract_keywords,
|
||||
temperature,
|
||||
keep_model_loaded,
|
||||
):
|
||||
@@ -90,6 +95,7 @@ class MiniCPM_VQA_Simple:
|
||||
torch_dtype=torch.bfloat16 if self.bf16_support else torch.float16,
|
||||
)
|
||||
|
||||
|
||||
with torch.no_grad():
|
||||
images = images.permute([0, 3, 1, 2])
|
||||
images = [ToPILImage()(img).convert("RGB") for img in images]
|
||||
@@ -113,6 +119,30 @@ class MiniCPM_VQA_Simple:
|
||||
# max_new_tokens=max_new_tokens,
|
||||
**params,
|
||||
)
|
||||
|
||||
keyword_result=""
|
||||
|
||||
if extract_keywords:#extract_keywords
|
||||
keyword_prompt = f"""Please extract keywords from the following text, including all occurrences of language (e.g. Chinese, English, etc.):
|
||||
[[[{result}]]]
|
||||
Please list the keywords extracted, separated by commas. Make sure to include all important words, no matter what language. For English words, please keep the original case."""
|
||||
|
||||
keyword_msgs = [{'role': 'user', 'content': keyword_prompt}]
|
||||
keyword_result = self.model.chat(
|
||||
image=None,
|
||||
msgs=keyword_msgs,
|
||||
tokenizer=self.tokenizer,
|
||||
sampling=True,
|
||||
# top_k=top_k,
|
||||
# top_p=top_p,
|
||||
temperature=temperature,
|
||||
# repetition_penalty=repetition_penalty,
|
||||
# max_new_tokens=max_new_tokens,
|
||||
**params,
|
||||
)
|
||||
print("keyword_result",keyword_result)
|
||||
|
||||
|
||||
# offload model to GPU
|
||||
# self.model = self.model.to(torch.device("cpu"))
|
||||
# self.model.eval()
|
||||
@@ -124,4 +154,4 @@ class MiniCPM_VQA_Simple:
|
||||
torch.cuda.empty_cache() # release GPU memory
|
||||
torch.cuda.ipc_collect()
|
||||
# print(result)
|
||||
return (result,)
|
||||
return (result,keyword_result,)
|
||||
|
||||
+26
-6
@@ -187,7 +187,8 @@ class PromptImage:
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("json_str",)
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@@ -202,12 +203,19 @@ class PromptImage:
|
||||
filename_prefix="mixlab_"
|
||||
filename_prefix += self.prefix_append
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
|
||||
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
|
||||
filename_prefix,self.output_dir, images[0].shape[1], images[0].shape[0])
|
||||
|
||||
full_output_folder=os.path.join(full_output_folder,'PromptImage')
|
||||
subfolder='PromptImage'
|
||||
|
||||
results = list()
|
||||
|
||||
save_to_image=save_to_image[0]=='enable'
|
||||
|
||||
#保存到本地的json文件,记录图片和prompt的对应关系
|
||||
output_images=[]
|
||||
output_prompt=[]
|
||||
|
||||
for index in range(len(images)):
|
||||
res=[]
|
||||
imgs=images[index]
|
||||
@@ -215,24 +223,36 @@ class PromptImage:
|
||||
for image in imgs:
|
||||
img=tensor2pil(image)
|
||||
|
||||
prompt_text=prompts[index]
|
||||
|
||||
metadata = None
|
||||
if save_to_image:
|
||||
metadata = PngInfo()
|
||||
prompt_text=prompts[index]
|
||||
if prompt_text is not None:
|
||||
metadata.add_text("prompt_text", prompt_text)
|
||||
|
||||
file = f"{filename}_{index}_{counter:05}_.png"
|
||||
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=self.compress_level)
|
||||
fp=os.path.join(full_output_folder,file)
|
||||
img.save(fp, pnginfo=metadata, compress_level=self.compress_level)
|
||||
res.append({
|
||||
"filename": file,
|
||||
"subfolder": subfolder,
|
||||
"type": self.type
|
||||
})
|
||||
output_images.append(fp)
|
||||
output_prompt.append(prompt_text)
|
||||
counter += 1
|
||||
results.append(res)
|
||||
|
||||
return { "ui": { "_images": results,"prompts":prompts } }
|
||||
|
||||
# if save_to_image:
|
||||
# # 保存为本地文件
|
||||
# with open(os.path.join(full_output_folder,'PromptImage.json'), 'w') as file:
|
||||
# json.dump(output_dict, file, ensure_ascii=False, indent=4)
|
||||
|
||||
return { "ui": { "_images": results,"prompts":prompts },"result":(json.dumps({
|
||||
"images":output_images,
|
||||
"prompts":output_prompt
|
||||
}),) }
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from huggingface_hub import snapshot_download
|
||||
import torch,re
|
||||
from sensevoice.onnx.sense_voice_ort_session import SenseVoiceInferenceSession
|
||||
from sensevoice.utils.frontend import WavFrontend
|
||||
from sensevoice.utils.fsmn_vad import FSMNVad
|
||||
import comfy.utils
|
||||
import folder_paths
|
||||
|
||||
languages = {"auto": 0, "zh": 3, "en": 4, "yue": 7, "ja": 11, "ko": 12, "nospeech": 13}
|
||||
|
||||
# 设置环境变量
|
||||
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
|
||||
|
||||
#
|
||||
def get_model_path():
|
||||
try:
|
||||
return folder_paths.get_folder_paths('sense_voice')[0]
|
||||
except:
|
||||
return os.path.join(folder_paths.models_dir, "sense_voice")
|
||||
|
||||
class AnyType(str):
|
||||
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
|
||||
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
any_type = AnyType("*")
|
||||
|
||||
# 字幕
|
||||
def format_to_srt(channel_id, start_time_ms, end_time_ms, asr_result):
|
||||
start_time = start_time_ms / 1000
|
||||
end_time = end_time_ms / 1000
|
||||
|
||||
def format_time(seconds):
|
||||
hours = int(seconds // 3600)
|
||||
minutes = int((seconds % 3600) // 60)
|
||||
seconds = seconds % 60
|
||||
milliseconds = int((seconds - int(seconds)) * 1000)
|
||||
return f"{hours:02}:{minutes:02}:{int(seconds):02},{milliseconds:03}"
|
||||
|
||||
start_time_str = format_time(start_time)
|
||||
end_time_str = format_time(end_time)
|
||||
|
||||
pattern = r"<\|(.+?)\|><\|(.+?)\|><\|(.+?)\|><\|(.+?)\|>(.+)"
|
||||
match = re.match(pattern,asr_result)
|
||||
print('#format_to_srt',match,asr_result)
|
||||
if match==None:
|
||||
return None, None, None, None,None,start_time,end_time,None
|
||||
lang, emotion, audio_type, itn, text = match.groups()
|
||||
# 😊 表示高兴,😡 表示愤怒,😔 表示悲伤。对于音频事件,🎼 表示音乐,😀 表示笑声,👏 表示掌声
|
||||
|
||||
srt_content = f"1\n{start_time_str} --> {end_time_str}\n{text}\n"
|
||||
|
||||
logging.info(f"[Channel {channel_id}] [{start_time}s - {end_time}s] [{lang}] [{emotion}] [{audio_type}] [{itn}] {text}")
|
||||
|
||||
return lang, emotion, audio_type, itn,srt_content,start_time,end_time,text
|
||||
|
||||
|
||||
class SenseVoiceProcessor:
|
||||
def __init__(self, download_model_path, device, num_threads, use_int8):
|
||||
|
||||
if not os.path.exists(download_model_path):
|
||||
logging.info(
|
||||
"Downloading model from huggingface hub from https://huggingface.co/lovemefan/SenseVoice-onnx"
|
||||
)
|
||||
logging.info(
|
||||
"You can speed up with `export HF_ENDPOINT=https://hf-mirror.com`"
|
||||
)
|
||||
snapshot_download(
|
||||
repo_id="lovemefan/SenseVoice-onnx", local_dir=download_model_path
|
||||
)
|
||||
|
||||
self.download_model_path = download_model_path
|
||||
self.device = device
|
||||
self.num_threads = num_threads
|
||||
self.use_int8 = use_int8
|
||||
self.front = WavFrontend(os.path.join(download_model_path, "am.mvn"))
|
||||
self.model = SenseVoiceInferenceSession(
|
||||
os.path.join(download_model_path, "embedding.npy"),
|
||||
os.path.join(
|
||||
download_model_path,
|
||||
"sense-voice-encoder-int8.onnx"
|
||||
if use_int8
|
||||
else "sense-voice-encoder.onnx",
|
||||
),
|
||||
os.path.join(download_model_path, "chn_jpn_yue_eng_ko_spectok.bpe.model"),
|
||||
device,
|
||||
num_threads,
|
||||
)
|
||||
self.vad = FSMNVad(download_model_path)
|
||||
|
||||
def process_audio(self, waveform, _sample_rate, language, use_itn):
|
||||
|
||||
start = time.time()
|
||||
pbar = comfy.utils.ProgressBar(waveform.shape[1]) # 进度条
|
||||
|
||||
results = []
|
||||
|
||||
for channel_id, channel_data in enumerate(waveform.T):
|
||||
segments = self.vad.segments_offline(channel_data)
|
||||
|
||||
for part in segments:
|
||||
audio_feats = self.front.get_features(channel_data[part[0] * 16 : part[1] * 16])
|
||||
asr_result = self.model(
|
||||
audio_feats[None, ...],
|
||||
language=languages[language],
|
||||
use_itn=use_itn,
|
||||
)
|
||||
|
||||
lang, emotion, audio_type, itn,srt_content,start_time,end_time,text=format_to_srt(
|
||||
channel_id,
|
||||
part[0] ,
|
||||
part[1],
|
||||
asr_result)
|
||||
|
||||
if lang!=None:
|
||||
results.append({
|
||||
"language":lang,
|
||||
"emotion":emotion,
|
||||
"audio_type":audio_type,
|
||||
"itn":itn,
|
||||
"srt_content":srt_content,
|
||||
"start_time":start_time,
|
||||
"end_time":end_time,
|
||||
"text":text
|
||||
})
|
||||
|
||||
self.vad.vad.all_reset_detection()
|
||||
pbar.update(1) # 更新进度条
|
||||
|
||||
decoding_time = time.time() - start
|
||||
logging.info(f"Decoder audio takes {decoding_time} seconds")
|
||||
logging.info(f"The RTF is {decoding_time/(waveform.shape[1] * len(waveform) / _sample_rate)}.")
|
||||
return results
|
||||
|
||||
|
||||
class SenseVoiceNode:
|
||||
|
||||
def __init__(self):
|
||||
self.processor = None
|
||||
self.download_model_path=get_model_path()
|
||||
self.device="cpu"
|
||||
self.num_threads = 4
|
||||
self.use_int8 = True
|
||||
self.language='auto'
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
|
||||
return {"required": {
|
||||
"audio": ("AUDIO", ),
|
||||
"device": ( ['auto','cpu'], {"default": 'auto'}),
|
||||
"language": (list(languages.keys()), {"default": 'auto'}),# 不能直接写 languages.keys(),json.dumps会报错
|
||||
"num_threads":("INT",{
|
||||
"default":4,
|
||||
"min": 1, #Minimum value
|
||||
"max": 32, #Maximum value
|
||||
"step": 1, #Slider's step
|
||||
"display": "number" # Cosmetic only: display as "number" or "slider"
|
||||
},),
|
||||
"use_int8":("BOOLEAN", {"default": True},),
|
||||
"use_itn":("BOOLEAN", {"default": True},),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "♾️Mixlab/Audio"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "run"
|
||||
|
||||
RETURN_TYPES = (any_type,"STRING","STRING","FLOAT",)
|
||||
RETURN_NAMES = ("result","srt","text","total_seconds",)
|
||||
|
||||
def run(self,audio,device,language,num_threads,use_int8,use_itn ):
|
||||
|
||||
if device!=self.device:
|
||||
self.device=device
|
||||
self.processor=None
|
||||
if language!=self.language:
|
||||
self.language=language
|
||||
self.processor=None
|
||||
if num_threads!=self.num_threads:
|
||||
self.num_threads=num_threads
|
||||
self.processor=None
|
||||
if use_int8!=self.use_int8:
|
||||
self.use_int8=use_int8
|
||||
self.processor=None
|
||||
|
||||
if device=='auto' and torch.cuda.is_available():
|
||||
self.device='cuda'
|
||||
|
||||
# num_threads=4
|
||||
# use_int8=True
|
||||
|
||||
if self.processor==None:
|
||||
self.processor = SenseVoiceProcessor(self.download_model_path,
|
||||
self.device,
|
||||
self.num_threads,
|
||||
self.use_int8)
|
||||
|
||||
if 'waveform' in audio and 'sample_rate' in audio:
|
||||
waveform = audio['waveform']
|
||||
sample_rate = audio['sample_rate']
|
||||
# print("Original shape:", waveform.shape) # 打印原始形状
|
||||
if waveform.ndim == 3 and waveform.shape[0] == 1: # 检查是否为三维且 batch_size 为 1
|
||||
waveform = waveform.squeeze(0) # 移除 batch_size 维度
|
||||
else:
|
||||
raise ValueError("Unexpected waveform dimensions")
|
||||
|
||||
print("waveform.shape:", waveform.shape)
|
||||
total_length_seconds = waveform.shape[1] / sample_rate
|
||||
|
||||
waveform_numpy = waveform.numpy().transpose(1, 0) # 转换为 (num_samples, num_channels)
|
||||
|
||||
results=self.processor.process_audio(waveform_numpy, sample_rate, language, use_itn)
|
||||
|
||||
srt_content="\n".join([s['srt_content'] for s in results])
|
||||
text="\n".join([s['text'] for s in results])
|
||||
|
||||
return (results,srt_content,text,total_length_seconds,)
|
||||
|
||||
@@ -648,6 +648,49 @@ class AppInfo:
|
||||
|
||||
|
||||
|
||||
class CreateJsonNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"key": ("STRING",{"multiline": False,"default": "data","dynamicPrompts": False}),
|
||||
"value":(any_type,),
|
||||
"save":("BOOLEAN", {"default": True},),
|
||||
},
|
||||
"optional":{
|
||||
"json_str":("STRING", {"forceInput": True,}),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("json_str",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/Output"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
INPUT_IS_LIST = False
|
||||
# OUTPUT_IS_LIST = (True,)
|
||||
|
||||
def run(self,key,value,save,json_str=None):
|
||||
data={}
|
||||
|
||||
data[key]=value
|
||||
|
||||
if json_str:
|
||||
json_obj = json.loads(json_str)
|
||||
data.update(json_obj)
|
||||
|
||||
if save:
|
||||
# 保存为本地文件
|
||||
with open(os.path.join(folder_paths.get_output_directory(),'data.json'), 'w') as file:
|
||||
json.dump(data, file, ensure_ascii=False, indent=4)
|
||||
|
||||
return (json.dumps(data),)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SwitchByIndex:
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import os,re
|
||||
import sys,time
|
||||
from pathlib import Path
|
||||
import torchaudio
|
||||
import hashlib
|
||||
import torch
|
||||
import folder_paths
|
||||
import comfy.utils
|
||||
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
class AnyType(str):
|
||||
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
|
||||
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
any_type = AnyType("*")
|
||||
|
||||
def get_model_dir(m):
|
||||
try:
|
||||
return folder_paths.get_folder_paths(m)[0]
|
||||
except:
|
||||
return os.path.join(folder_paths.models_dir, m)
|
||||
|
||||
|
||||
|
||||
whisper_model_path=get_model_dir('whisper')
|
||||
|
||||
model_sizes=[
|
||||
d for d in os.listdir(whisper_model_path) if os.path.isdir(
|
||||
os.path.join(whisper_model_path, d)
|
||||
) and os.path.isfile(os.path.join(os.path.join(whisper_model_path, d), "config.json"))
|
||||
]
|
||||
|
||||
|
||||
class LoadWhisperModel:
|
||||
def __init__(self):
|
||||
self.model = None
|
||||
self.device="cuda" if torch.cuda.is_available() else "cpu"
|
||||
self.model_size=model_sizes[0]
|
||||
self.compute_type='float16'
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"model_size": (model_sizes,),
|
||||
"device": (["auto","cpu"],),
|
||||
"compute_type": (["float16","int8_float16","int8"],),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("WHISPER",)
|
||||
RETURN_NAMES = ("whisper_model",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/Audio/Whisper"
|
||||
|
||||
INPUT_IS_LIST = False
|
||||
OUTPUT_IS_LIST = (False,)
|
||||
|
||||
def run(self,model_size,device,compute_type):
|
||||
|
||||
if device=="auto" and self.device!='cuda':
|
||||
self.device="cuda" if torch.cuda.is_available() else "cpu"
|
||||
self.model=None
|
||||
|
||||
if device=='cpu' and self.device!='cpu':
|
||||
self.device="cpu"
|
||||
self.model=None
|
||||
|
||||
if model_size!= self.model_size:
|
||||
self.model_size=model_size
|
||||
self.model=None
|
||||
|
||||
if compute_type!=self.compute_type:
|
||||
self.compute_type=compute_type
|
||||
self.model=None
|
||||
|
||||
if self.model==None:
|
||||
self.model = WhisperModel(
|
||||
os.path.join(whisper_model_path, self.model_size),
|
||||
device=self.device,
|
||||
compute_type=self.compute_type
|
||||
)
|
||||
|
||||
return (self.model,)
|
||||
|
||||
|
||||
class WhisperTranscribe:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"whisper_model": ("WHISPER",),
|
||||
"audio": ("AUDIO",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type,"STRING","STRING","FLOAT",)
|
||||
RETURN_NAMES = ("result","srt","text","total_seconds",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/Audio/Whisper"
|
||||
|
||||
INPUT_IS_LIST = False
|
||||
# OUTPUT_IS_LIST = (False,False,False,)
|
||||
|
||||
def run(self,whisper_model,audio):
|
||||
|
||||
if 'audio_path' in audio and (not 'waveform' in audio):
|
||||
waveform, sample_rate = torchaudio.load(audio['audio_path'])
|
||||
waveform=waveform.mean(0)
|
||||
total_length_seconds = waveform.shape[0] / sample_rate
|
||||
waveform=waveform.numpy()
|
||||
|
||||
elif 'waveform' in audio and 'sample_rate' in audio:
|
||||
print("Original shape:", audio["waveform"].shape, isinstance(audio["waveform"], torch.Tensor)) # 打印原始形状
|
||||
waveform = audio["waveform"].squeeze(0) # Remove the added batch dimension
|
||||
sample_rate = audio["sample_rate"]
|
||||
|
||||
# if audio_sf != sampling_rate:
|
||||
# waveform = torchaudio.functional.resample(
|
||||
# waveform, orig_freq=audio_sf, new_freq=sampling_rate
|
||||
# )
|
||||
|
||||
waveform=waveform.mean(0)
|
||||
|
||||
total_length_seconds = waveform.shape[0] / sample_rate
|
||||
|
||||
waveform=waveform.numpy() #whisper_model.transcribe 旧版不支持直接传tensor,先用numpy
|
||||
|
||||
segments, info = whisper_model.transcribe(waveform, beam_size=5)
|
||||
|
||||
print("Detected language '%s' with probability %f" % (info.language, info.language_probability))
|
||||
|
||||
# Function to format time for SRT
|
||||
def format_time(seconds):
|
||||
millis = int((seconds - int(seconds)) * 1000)
|
||||
hours, remainder = divmod(int(seconds), 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
return f"{hours:02}:{minutes:02}:{seconds:02},{millis:03}"
|
||||
|
||||
# Prepare SRT content as a string
|
||||
results = []
|
||||
for i, segment in enumerate(segments):
|
||||
start_time = format_time(segment.start)
|
||||
end_time = format_time(segment.end)
|
||||
srt_content = f"{i + 1}\n"
|
||||
srt_content += f"{start_time} --> {end_time}\n"
|
||||
|
||||
text=segment.text.strip()
|
||||
|
||||
srt_content += f"{text}\n\n"
|
||||
|
||||
start_time=segment.start
|
||||
end_time=segment.end
|
||||
|
||||
|
||||
results.append({
|
||||
"srt_content":srt_content,
|
||||
"start_time":start_time,
|
||||
"end_time":end_time,
|
||||
"text":text,
|
||||
"language":[info.language]
|
||||
})
|
||||
|
||||
srt_content="\n".join([s['srt_content'] for s in results])
|
||||
text="\n".join([s['text'] for s in results])
|
||||
|
||||
return (results,srt_content,text,total_length_seconds,)
|
||||
|
||||
@@ -132,6 +132,9 @@ def split_video_by_scenes(video_path, scenes, output_path, number_of_sample_fram
|
||||
width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
|
||||
# 视频的总帧数
|
||||
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
|
||||
# Create a list to hold the paths of the scene videos
|
||||
scenes_video = []
|
||||
keyframes = []
|
||||
@@ -194,7 +197,7 @@ def split_video_by_scenes(video_path, scenes, output_path, number_of_sample_fram
|
||||
# Release the video file
|
||||
video.release()
|
||||
|
||||
return scenes_video, keyframes
|
||||
return scenes_video, keyframes,total_frames
|
||||
|
||||
|
||||
def get_files_with_extension(directory, extension):
|
||||
@@ -287,8 +290,8 @@ class ScenedetectNode_:
|
||||
"number_of_sample_frames": ("INT", {"default": 1, "min": 1, "step": 1}), # 抽取的帧数,默认是1帧,中间帧
|
||||
},}
|
||||
|
||||
RETURN_TYPES = ("SCENE_VIDEO","SCENE_", "INT",)
|
||||
RETURN_NAMES = ("scenes_video","scenes","scene_len",)
|
||||
RETURN_TYPES = ("SCENE_VIDEO","SCENE_", "INT","INT",)
|
||||
RETURN_NAMES = ("scenes_video","scenes","scene_len","total_frames",)
|
||||
OUTPUT_IS_LIST = (False,False,False,)
|
||||
|
||||
FUNCTION = "run"
|
||||
@@ -310,9 +313,9 @@ class ScenedetectNode_:
|
||||
folder_path = create_folder(tp,name_without_extension)
|
||||
# print("New folder created:", folder_path)
|
||||
|
||||
vs_files,keyframes=split_video_by_scenes(video_path,scenes,folder_path,number_of_sample_frames)
|
||||
vs_files,keyframes,total=split_video_by_scenes(video_path,scenes,folder_path,number_of_sample_frames)
|
||||
# print("New folder created:", vs_files)
|
||||
|
||||
return (vs_files,keyframes,len(scenes),)
|
||||
return (vs_files,keyframes,len(scenes),total,)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-mixlab-nodes"
|
||||
description = "3D, ScreenShareNode & FloatingVideoNode, SpeechRecognition & SpeechSynthesis, GPT, LoadImagesFromLocal, Layers, Other Nodes, ..."
|
||||
version = "0.41.1"
|
||||
version = "0.44.0"
|
||||
license = "MIT"
|
||||
dependencies = ["numpy", "pyOpenSSL", "watchdog", "opencv-python-headless", "matplotlib", "openai", "simple-lama-inpainting", "clip-interrogator==0.6.0", "transformers>=4.36.0", "lark-parser", "imageio-ffmpeg", "rembg[gpu]", "omegaconf==2.3.0", "Pillow>=9.5.0", "einops==0.7.0", "trimesh>=4.0.5", "huggingface-hub", "scikit-image"]
|
||||
|
||||
|
||||
+6
-1
@@ -27,4 +27,9 @@ scenedetect[opencv-headless]
|
||||
hydra-core>=1.3.2
|
||||
loralib>=0.1.2
|
||||
natsort>=8.4.0
|
||||
# simple-lama-inpainting
|
||||
|
||||
#simple-lama-inpainting
|
||||
|
||||
git+https://github.com/shadowcz007/SenseVoice-python.git
|
||||
|
||||
faster_whisper
|
||||
|
||||
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
|
||||
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
|
||||
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
|
||||
|
||||
const version = 'v0.41.1'
|
||||
const version = 'v0.44.0'
|
||||
|
||||
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -6,6 +6,10 @@ window._bg_img = null
|
||||
* draws the back canvas (the one containing the background and the connections)
|
||||
* @method drawBackCanvas
|
||||
**/
|
||||
|
||||
// 判断是否是新版的,LGraphCanvas.prototype.drawBackCanvas.toString().match('window.devicePixelRatio')
|
||||
let scale=LGraphCanvas.prototype.drawBackCanvas.toString().match('window.devicePixelRatio')?window.devicePixelRatio:1;
|
||||
|
||||
LGraphCanvas.prototype.drawBackCanvas = function () {
|
||||
var canvas = this.bgcanvas
|
||||
if (
|
||||
@@ -60,7 +64,7 @@ LGraphCanvas.prototype.drawBackCanvas = function () {
|
||||
if (!this.viewport) {
|
||||
ctx.restore()
|
||||
// ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||
ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0)
|
||||
ctx.setTransform(scale, 0, 0, scale, 0, 0)
|
||||
}
|
||||
this.visible_links.length = 0
|
||||
|
||||
|
||||
@@ -544,7 +544,7 @@ async function getCustomnodeMappings () {
|
||||
const data = (await get_nodes_map()).data
|
||||
window._nodes_maps = data
|
||||
}
|
||||
console.log('#getCustomnodeMappings', window._nodes_maps)
|
||||
// console.log('#getCustomnodeMappings', window._nodes_maps)
|
||||
for (let url in window._nodes_maps) {
|
||||
let n = window._nodes_maps[url]
|
||||
for (let node of n[0]) {
|
||||
|
||||
@@ -41,6 +41,9 @@ class Visualizer {
|
||||
overflow: 'hidden'
|
||||
})
|
||||
this.iframe.src = '/mixlab/app/' + visualSrc + '.html'
|
||||
// this.iframe.width="300";
|
||||
// this.iframe.height="400";
|
||||
|
||||
console.log('#Visualizer', container, this.iframe)
|
||||
container.appendChild(this.iframe)
|
||||
}
|
||||
@@ -73,7 +76,7 @@ function createVisualizer (node, inputName, typeName, inputData, app) {
|
||||
draw: function (ctx, node, widgetWidth, widgetY, widgetHeight) {
|
||||
const margin = 10
|
||||
const top_offset = 5
|
||||
const visible = app.canvas.ds.scale > 0.5 && this.type === typeName
|
||||
const visible = app.canvas.ds.scale > 0.3 && this.type === typeName
|
||||
const w = widgetWidth - margin * 4
|
||||
const clientRectBound = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
@@ -85,12 +88,13 @@ function createVisualizer (node, inputName, typeName, inputData, app) {
|
||||
.translateSelf(margin, margin + widgetY)
|
||||
|
||||
Object.assign(this.visualizer.style, {
|
||||
left: `${transform.a * margin + transform.e + 40}px`,
|
||||
left: `${transform.a * margin + transform.e + 0}px`,
|
||||
top: `${transform.d + transform.f + top_offset}px`,
|
||||
width: `${w * transform.a}px`,
|
||||
height: `${
|
||||
w * transform.d - widgetHeight - margin * 15 * transform.d
|
||||
}px`,
|
||||
height: `${(w * transform.a * 4) / 3 - margin * 5 * transform.d}px`,
|
||||
// height: `${
|
||||
// w * transform.d - widgetHeight - margin * 15 * transform.d
|
||||
// }px`,
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
zIndex: app.graph._nodes.indexOf(node)
|
||||
@@ -137,11 +141,11 @@ function createVisualizer (node, inputName, typeName, inputData, app) {
|
||||
// Make sure visualization iframe is always inside the node when resize the node
|
||||
node.onResize = function () {
|
||||
let [w, h] = this.size
|
||||
if (w <= 600) w = 600
|
||||
if (h <= 500) h = 500
|
||||
if (w <= 300) w = 300
|
||||
if (h <= 400) h = 400
|
||||
|
||||
if (w > 600) {
|
||||
h = w - 100
|
||||
if (w > 300) {
|
||||
h = Math.round((w * 4) / 3)
|
||||
}
|
||||
|
||||
this.size = [w, h]
|
||||
@@ -181,14 +185,14 @@ function registerVisualizer (nodeType, nodeData, nodeClassName, typeName) {
|
||||
app
|
||||
])
|
||||
|
||||
this.setSize([600, 500])
|
||||
this.setSize([300, 400])
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
nodeType.prototype.onExecuted = async function (message) {
|
||||
// Check if reference image and depth map are available
|
||||
console.log("#message",message)
|
||||
console.log('#message', message)
|
||||
if (message.reference_image && message.depth_map) {
|
||||
const params = {}
|
||||
params.reference_image = message.reference_image[0]
|
||||
|
||||
+2
-2
@@ -2026,8 +2026,8 @@
|
||||
var iframe = document.createElement('iframe')
|
||||
iframe.src = "https://mememagic-editor.vercel.app/"
|
||||
iframe.setAttribute('frameborder', '0')
|
||||
iframe.setAttribute('width', '500')
|
||||
iframe.setAttribute('height', '700')
|
||||
iframe.setAttribute('width', '700')
|
||||
iframe.setAttribute('height', '720')
|
||||
iframe.setAttribute('allow',"clipboard-read; clipboard-write")
|
||||
|
||||
|
||||
|
||||
@@ -223,6 +223,10 @@
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.image-with-grid img {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* .card:hover {
|
||||
box-shadow: 0px 0px 10px 10px #e9fbfa;
|
||||
} */
|
||||
|
||||
@@ -237,8 +237,11 @@ const sleep = (t = 1000) => {
|
||||
// 方法:旋转摄像机并拍摄图片 // 每次旋转的角度增量,转换为弧度
|
||||
async function captureImages (
|
||||
totalFrames = 20,
|
||||
angleIncrement = THREE.MathUtils.degToRad(1.5)
|
||||
angleIncrement = 1.5,
|
||||
scaleFactor = 1 // 添加放大倍数参数,默认为1
|
||||
) {
|
||||
angleIncrement = THREE.MathUtils.degToRad(angleIncrement)
|
||||
|
||||
// 计算场景中所有物体的中心点
|
||||
const box = new THREE.Box3().setFromObject(scene)
|
||||
const center = new THREE.Vector3()
|
||||
@@ -264,6 +267,21 @@ async function captureImages (
|
||||
const startAngle = initialAngle
|
||||
// - (angleIncrement * totalFrames) / 2
|
||||
|
||||
// 保存原始尺寸
|
||||
const originalWidth = renderer.domElement.width
|
||||
const originalHeight = renderer.domElement.height
|
||||
|
||||
// 调整渲染器尺寸
|
||||
renderer.setSize(
|
||||
originalWidth * scaleFactor,
|
||||
originalHeight * scaleFactor,
|
||||
false
|
||||
)
|
||||
|
||||
// 调整相机的视图矩阵(如果需要)
|
||||
camera.aspect = (originalWidth * scaleFactor) / (originalHeight * scaleFactor)
|
||||
camera.updateProjectionMatrix()
|
||||
|
||||
for (let i = 0; i < totalFrames; i++) {
|
||||
const angle = startAngle + i * angleIncrement
|
||||
|
||||
@@ -284,6 +302,13 @@ async function captureImages (
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
}
|
||||
|
||||
// 恢复渲染器尺寸
|
||||
renderer.setSize(originalWidth, originalHeight, false)
|
||||
|
||||
// 恢复相机的视图矩阵
|
||||
camera.aspect = originalWidth / originalHeight
|
||||
camera.updateProjectionMatrix()
|
||||
|
||||
// 恢复相机到初始位置和朝向
|
||||
camera.position.copy(initialPosition)
|
||||
camera.lookAt(initialTarget)
|
||||
@@ -294,7 +319,7 @@ async function captureImages (
|
||||
async function takeScreenshot () {
|
||||
// 更新相机的矩阵,以确保其世界矩阵是最新的
|
||||
camera.updateMatrixWorld()
|
||||
const imgs = await captureImages()
|
||||
const imgs = await captureImages(12,3,4)
|
||||
|
||||
// 获取当前网页的 URL
|
||||
const currentUrl = window.location.href
|
||||
|
||||
Reference in New Issue
Block a user