Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bac87ee52 | ||
|
|
9a01701019 | ||
|
|
693954ee23 | ||
|
|
bf4ba91e7a | ||
|
|
0828353253 | ||
|
|
1997c7ad8f | ||
|
|
b1e62440e4 | ||
|
|
d549a5eb6a | ||
|
|
77bfb08d76 |
@@ -6,10 +6,7 @@
|
||||
|
||||
##### `最新`:
|
||||
|
||||
- 增加 Edit Mask,方便在生成的时候手动绘制 mask [workflow](./workflow/edit-mask-workflow.json)
|
||||
|
||||
|
||||
- ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。模型下载后,放置到 `models/llamafile/`
|
||||
ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。模型下载后,放置到 `models/llamafile/`
|
||||
|
||||
- 右键菜单支持 text-to-text,方便对 prompt 词补全
|
||||
|
||||
@@ -26,11 +23,7 @@
|
||||
|
||||
#### `相关插件推荐`
|
||||
|
||||
[comfyui-liveportrait](https://github.com/shadowcz007/comfyui-liveportrait)
|
||||
|
||||
[Comfyui-ChatTTS](https://github.com/shadowcz007/Comfyui-ChatTTS)
|
||||
|
||||
[comfyui-sound-lab](https://github.com/shadowcz007/comfyui-sound-lab)
|
||||
<!-- [comfyui-sd-prompt-mixlab](https://github.com/shadowcz007/comfyui-sd-prompt-mixlab) -->
|
||||
|
||||
[comfyui-Image-reward](https://github.com/shadowcz007/comfyui-Image-reward)
|
||||
|
||||
@@ -47,7 +40,6 @@
|
||||
- 发布为 app 的 workflow,可以在右键里再次编辑了
|
||||
- web app 可以设置分类,在 comfyui 右键菜单可以编辑更新 web app
|
||||
- 支持动态提示
|
||||
- 支持把输出显示到comfyui背景(TouchDesigner 风格)
|
||||
|
||||

|
||||
|
||||
@@ -169,9 +161,6 @@ pip install llama-cpp-python \
|
||||
|
||||
> A new layer class node has been added, allowing you to separate the image into layers. After merging the images, you can input the controlnet for further processing.
|
||||
|
||||
> The composite images node overlays a foreground image onto a background image at specified positions and scales, with optional blending modes and masking capabilities. position : 'overall',"center_center","left_bottom","center_bottom","right_bottom","left_top","center_top","right_top"
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
@@ -203,12 +192,6 @@ pip install llama-cpp-python \
|
||||
|
||||
> Conveniently load images from a fixed address on the internet to ensure that default images in the workflow can be executed.
|
||||
|
||||
#### TextImage
|
||||
|
||||
> [下载字体](https://drxie.github.io/OSFCC/)放到 ```custom_nodes/comfyui-mixlab-nodes/assets/fonts```
|
||||
|
||||
|
||||
|
||||
### Style
|
||||
|
||||
> Apply VisualStyle Prompting , Modified from [ComfyUI_VisualStylePrompting](https://github.com/ExponentialML/ComfyUI_VisualStylePrompting)
|
||||
|
||||
+139
-143
@@ -1,4 +1,3 @@
|
||||
#
|
||||
import os
|
||||
import subprocess
|
||||
import importlib.util
|
||||
@@ -13,11 +12,13 @@ from PIL import Image
|
||||
from comfy.cli_args import args
|
||||
python = sys.executable
|
||||
|
||||
# print("sys.path", sys.path)
|
||||
|
||||
#修复 sys.stdout.isatty() object has no attribute 'isatty'
|
||||
try:
|
||||
sys.stdout.isatty()
|
||||
except:
|
||||
print('#fix sys.stdout.isatty')
|
||||
# print('#fix sys.stdout.isatty')
|
||||
sys.stdout.isatty = lambda: False
|
||||
|
||||
llama_port=None
|
||||
@@ -179,7 +180,7 @@ def create_for_https():
|
||||
|
||||
# workflow 目录下的所有json
|
||||
def read_workflow_json_files_all(folder_path):
|
||||
# print('#read_workflow_json_files_all',folder_path)
|
||||
print('#read_workflow_json_files_all',folder_path)
|
||||
json_files = []
|
||||
for root, dirs, files in os.walk(folder_path):
|
||||
for file in files:
|
||||
@@ -575,7 +576,7 @@ async def mixlab_app_handler(request):
|
||||
return web.Response(text=html_data, content_type='text/html')
|
||||
else:
|
||||
return web.Response(text="HTML file not found", status=404)
|
||||
|
||||
|
||||
|
||||
@routes.post('/mixlab/workflow')
|
||||
async def mixlab_workflow_hander(request):
|
||||
@@ -718,6 +719,44 @@ async def post_prompt_result(request):
|
||||
return web.json_response({"result":res})
|
||||
|
||||
|
||||
|
||||
def start_local_live_thread(data):
|
||||
import asyncio
|
||||
from VoiceStreamAI.server import Server
|
||||
from VoiceStreamAI.asr.asr_factory import ASRFactory
|
||||
from VoiceStreamAI.vad.vad_factory import VADFactory
|
||||
|
||||
model="large-v3"
|
||||
if "model" in data:
|
||||
model=data['model']
|
||||
|
||||
vad_pipeline = VADFactory.create_vad_pipeline("pyannote")
|
||||
#device
|
||||
asr_pipeline = ASRFactory.create_asr_pipeline("faster_whisper", **{"model_size":model})
|
||||
|
||||
port=8765
|
||||
if 'port' in data:
|
||||
port=data['port']
|
||||
|
||||
llm_port=9000
|
||||
if 'llm_port' in data:
|
||||
llm_port=data['llm_port']
|
||||
|
||||
server = Server(vad_pipeline,
|
||||
asr_pipeline,
|
||||
host="127.0.0.1",
|
||||
port=port,
|
||||
sampling_rate=16000,
|
||||
samples_width=2,
|
||||
llm_port=llm_port
|
||||
)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(server.start())
|
||||
loop.run_forever()
|
||||
|
||||
|
||||
async def start_local_llm(data):
|
||||
global llama_port,llama_model,llama_chat_format
|
||||
if llama_port and llama_model and llama_chat_format:
|
||||
@@ -747,6 +786,8 @@ async def start_local_llm(data):
|
||||
|
||||
|
||||
chat_format="chatml"
|
||||
if "model" in data and "function-calling" in data['model']:
|
||||
chat_format="functionary-v2"
|
||||
|
||||
model_alias=os.path.basename(model)
|
||||
|
||||
@@ -760,7 +801,8 @@ async def start_local_llm(data):
|
||||
if file.startswith(file_name):
|
||||
clip_model_path=os.path.join(os.path.dirname(model),file)
|
||||
chat_format='llava-1-5'
|
||||
# print('#clip_model_path',chat_format,clip_model_path,model)
|
||||
print('#clip_model_path',chat_format,clip_model_path)
|
||||
|
||||
|
||||
address="127.0.0.1"
|
||||
port=9090
|
||||
@@ -778,39 +820,38 @@ async def start_local_llm(data):
|
||||
server_settings=ServerSettings(host=address,port=port)
|
||||
|
||||
name, ext = os.path.splitext(os.path.basename(model))
|
||||
if name:
|
||||
# print('#model',name)
|
||||
app = create_app(
|
||||
server_settings=server_settings,
|
||||
model_settings=[
|
||||
ModelSettings(
|
||||
model=model,
|
||||
model_alias=name,
|
||||
n_gpu_layers=n_gpu_layers,
|
||||
n_ctx=4098,
|
||||
chat_format=chat_format,
|
||||
embedding=False,
|
||||
clip_model_path=clip_model_path
|
||||
)])
|
||||
print('#model',name)
|
||||
app = create_app(
|
||||
server_settings=server_settings,
|
||||
model_settings=[
|
||||
ModelSettings(
|
||||
model=model,
|
||||
model_alias=name,
|
||||
n_gpu_layers=n_gpu_layers,
|
||||
n_ctx=4098,
|
||||
chat_format=chat_format,
|
||||
embedding=False,
|
||||
clip_model_path=clip_model_path
|
||||
)])
|
||||
|
||||
def run_uvicorn():
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=os.getenv("HOST", server_settings.host),
|
||||
port=int(os.getenv("PORT", server_settings.port)),
|
||||
ssl_keyfile=server_settings.ssl_keyfile,
|
||||
ssl_certfile=server_settings.ssl_certfile,
|
||||
)
|
||||
def run_uvicorn():
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=os.getenv("HOST", server_settings.host),
|
||||
port=int(os.getenv("PORT", server_settings.port)),
|
||||
ssl_keyfile=server_settings.ssl_keyfile,
|
||||
ssl_certfile=server_settings.ssl_certfile,
|
||||
)
|
||||
|
||||
# 创建一个子线程
|
||||
thread = threading.Thread(target=run_uvicorn)
|
||||
# 创建一个子线程
|
||||
thread = threading.Thread(target=run_uvicorn)
|
||||
|
||||
# 启动子线程
|
||||
thread.start()
|
||||
# 启动子线程
|
||||
thread.start()
|
||||
|
||||
llama_port=port
|
||||
llama_model=data['model']
|
||||
llama_chat_format=chat_format
|
||||
llama_port=port
|
||||
llama_model=data['model']
|
||||
llama_chat_format=chat_format
|
||||
|
||||
return {"port":llama_port,"model":llama_model,"chat_format":llama_chat_format}
|
||||
|
||||
@@ -829,6 +870,42 @@ async def my_hander_method(request):
|
||||
|
||||
return web.json_response(result)
|
||||
|
||||
|
||||
@routes.post('/mixlab/start_live')
|
||||
async def mixlab_live_start_handler(request):
|
||||
import threading
|
||||
llm=await start_local_llm({
|
||||
"model":"Phi-3-mini-4k-instruct-Q5_K_S.gguf",
|
||||
"n_gpu_layers":2
|
||||
})
|
||||
|
||||
# {"port":llama_port,"model":llama_model,"chat_format":llama_chat_format}
|
||||
|
||||
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com' #hf_hub_download 里的下载地址修改
|
||||
os.environ['PYANNOTE_AUTH_TOKEN'] = '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'
|
||||
|
||||
# Create and start the thread
|
||||
data = {
|
||||
"llm_port":llm['port'],
|
||||
"port":8725,
|
||||
"model":"large-v3"
|
||||
} # Replace with your actual data if needed
|
||||
thread = threading.Thread(target=start_local_live_thread, args=(data,))
|
||||
thread.start()
|
||||
|
||||
return web.json_response(data)
|
||||
|
||||
|
||||
@routes.get('/mixlab/live')
|
||||
async def mixlab_live_handler(request):
|
||||
html_file = os.path.join(current_path, "web/live.html")
|
||||
if os.path.exists(html_file):
|
||||
with open(html_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
html_data = f.read()
|
||||
return web.Response(text=html_data, content_type='text/html')
|
||||
else:
|
||||
return web.Response(text="HTML file not found", status=404)
|
||||
|
||||
# 重启服务
|
||||
@routes.post('/mixlab/re_start')
|
||||
def re_start(request):
|
||||
@@ -842,16 +919,20 @@ def re_start(request):
|
||||
|
||||
# 导入节点
|
||||
from .nodes.PromptNode import GLIGENTextBoxApply_Advanced,EmbeddingPrompt,RandomPrompt,PromptSlide,PromptSimplification,PromptImage,JoinWithDelimiter
|
||||
from .nodes.ImageNode import ImageListToBatch_,ComparingTwoFrames,LoadImages_,CompositeImages,GridDisplayAndSave,GridInput,ImagesPrompt,SaveImageAndMetadata,SaveImageToLocal,SplitImage,GridOutput,GetImageSize_,MirroredImage,ImageColorTransfer,NoiseImage,TransparentImage,GradientImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,CenterImage,AreaToMask,SmoothMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
|
||||
from .nodes.ImageNode import ComparingTwoFrames,LoadImages_,CompositeImages,GridDisplayAndSave,GridInput,ImagesPrompt,SaveImageAndMetadata,SaveImageToLocal,SplitImage,GridOutput,GetImageSize_,MirroredImage,ImageColorTransfer,NoiseImage,TransparentImage,GradientImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,CenterImage,AreaToMask,SmoothMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
|
||||
# from .nodes.Vae import VAELoader,VAEDecode
|
||||
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
|
||||
|
||||
from .nodes.Audio import AudioPlayNode,SpeechRecognition,SpeechSynthesis
|
||||
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText,TextSplitByDelimiter
|
||||
from .nodes.Audio import GamePal,SpeechRecognition,SpeechSynthesis
|
||||
from .nodes.Utils import 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
|
||||
|
||||
from .nodes.Video import VideoCombine_Adv,LoadVideoAndSegment,ImageListReplace,VAEEncodeForInpaint_Frames
|
||||
|
||||
from .nodes.TripoSR import LoadTripoSRModel,TripoSRSampler,SaveTripoSRMesh
|
||||
|
||||
|
||||
# 要导出的所有节点及其名称的字典
|
||||
@@ -882,7 +963,6 @@ NODE_CLASS_MAPPINGS = {
|
||||
"ImageColorTransfer":ImageColorTransfer,
|
||||
"ShowLayer":ShowLayer,
|
||||
"NewLayer":NewLayer,
|
||||
"ImageListToBatch_":ImageListToBatch_,
|
||||
"CompositeImages_":CompositeImages,
|
||||
"SplitImage":SplitImage,
|
||||
"CenterImage":CenterImage,
|
||||
@@ -904,7 +984,10 @@ NODE_CLASS_MAPPINGS = {
|
||||
# "VAEDecodeConsistencyDecoder":VAEDecode,
|
||||
"ScreenShare":ScreenShareNode,
|
||||
"FloatingVideo":FloatingVideo,
|
||||
|
||||
"ChatGPTOpenAI":ChatGPTNode,
|
||||
"ShowTextForGPT":ShowTextForGPT,
|
||||
"CharacterInText":CharacterInText,
|
||||
"TextSplitByDelimiter":TextSplitByDelimiter,
|
||||
"SpeechRecognition":SpeechRecognition,
|
||||
"SpeechSynthesis":SpeechSynthesis,
|
||||
"Color":ColorInput,
|
||||
@@ -928,21 +1011,24 @@ NODE_CLASS_MAPPINGS = {
|
||||
"ApplyVisualStylePrompting_":ApplyVisualStylePrompting,
|
||||
"StyleAlignedReferenceSampler_": StyleAlignedReferenceSampler,
|
||||
"StyleAlignedSampleReferenceLatents_": StyleAlignedSampleReferenceLatents,
|
||||
"StyleAlignedBatchAlign_": StyleAlignedBatchAlign,
|
||||
"StyleAlignedBatchAlign_": StyleAlignedBatchAlign,
|
||||
"LoadVideoAndSegment_":LoadVideoAndSegment,
|
||||
"VideoCombine_Adv":VideoCombine_Adv,
|
||||
"ListSplit_":ListSplit,
|
||||
"MaskListReplace_":MaskListReplace,
|
||||
"MaskListReplace_":MaskListReplace,
|
||||
"ImageListReplace_":ImageListReplace,
|
||||
"VAEEncodeForInpaint_Frames":VAEEncodeForInpaint_Frames,
|
||||
"IncrementingListNode_":IncrementingListNode,
|
||||
"PreviewMask_":PreviewMask_,
|
||||
"AudioPlay":AudioPlayNode
|
||||
"LoadTripoSRModel_": LoadTripoSRModel,
|
||||
"TripoSRSampler_": TripoSRSampler,
|
||||
"SaveTripoSRMesh": SaveTripoSRMesh
|
||||
# "GamePal":GamePal
|
||||
}
|
||||
|
||||
# 一个包含节点友好/可读的标题的字典
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"AppInfo":"App Info ♾️MixlabApp",
|
||||
"ScreenShare":"Screen Share ♾️Mixlab",
|
||||
"FloatingVideo":"Floating Video ♾️Mixlab",
|
||||
"TextImage":"Text Image ♾️Mixlab",
|
||||
|
||||
"Color":"Color Input ♾️MixlabApp",
|
||||
"TextInput_":"Text Input ♾️MixlabApp",
|
||||
"FloatSlider":"Float Slider Input ♾️MixlabApp",
|
||||
@@ -956,13 +1042,14 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SplitLongMask":"Splitting a long image into sections",
|
||||
"VAELoaderConsistencyDecoder":"Consistency Decoder Loader",
|
||||
"VAEDecodeConsistencyDecoder":"Consistency Decoder Decode",
|
||||
|
||||
|
||||
"ScreenShare":"Screen Share ♾️Mixlab",
|
||||
"FloatingVideo":"FloatingVideo ♾️Mixlab",
|
||||
"ChatGPTOpenAI":"ChatGPT & Local LLM ♾️Mixlab",
|
||||
"ShowTextForGPT":"Show Text ♾️MixlabApp",
|
||||
"MergeLayers":"Merge Layers ♾️Mixlab",
|
||||
"SpeechSynthesis":"SpeechSynthesis ♾️Mixlab",
|
||||
"SpeechRecognition":"SpeechRecognition ♾️Mixlab",
|
||||
"3DImage":"3DImage ♾️Mixlab",
|
||||
"ImageListToBatch_":"Image List To Batch",
|
||||
"CompositeImages_":"Composite Images ♾️Mixlab",
|
||||
"DynamicDelayProcessor":"DynamicDelayByText ♾️Mixlab",
|
||||
"LaMaInpainting":"LaMaInpainting ♾️Mixlab",
|
||||
@@ -988,12 +1075,13 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GridInput":"Grid Input ♾️Mixlab",
|
||||
"GridOutput":"Grid Output ♾️Mixlab",
|
||||
"GetImageSize_":"Get Image Size ♾️Mixlab",
|
||||
"VAEEncodeForInpaint_Frames":"VAE Encode For Inpaint Frames ♾️Mixlab",
|
||||
"IncrementingListNode_":"Create Incrementing Number List ♾️Mixlab",
|
||||
"LoadImagesToBatch":"Load Images(base64) ♾️Mixlab",
|
||||
"PreviewMask_":"Preview Mask",
|
||||
"AudioPlay":"Audio Play ♾️Mixlab",
|
||||
|
||||
"MultiplicationNode":"Math Operation ♾️Mixlab",
|
||||
"LoadTripoSRModel_": "Load TripoSR Model",
|
||||
"TripoSRSampler_": "TripoSR Sampler",
|
||||
"SaveTripoSRMesh": "Save TripoSR Mesh"
|
||||
}
|
||||
|
||||
# web ui的节点功能
|
||||
@@ -1004,41 +1092,6 @@ logging.info('--------------')
|
||||
logging.info('\033[91m ### Mixlab Nodes: \033[93mLoaded')
|
||||
# print('\033[91m ### Mixlab Nodes: \033[93mLoaded')
|
||||
|
||||
try:
|
||||
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText,TextSplitByDelimiter
|
||||
logging.info('ChatGPT.available True')
|
||||
|
||||
NODE_CLASS_MAPPINGS_V = {
|
||||
"ChatGPTOpenAI":ChatGPTNode,
|
||||
"ShowTextForGPT":ShowTextForGPT,
|
||||
"CharacterInText":CharacterInText,
|
||||
"TextSplitByDelimiter":TextSplitByDelimiter,
|
||||
}
|
||||
|
||||
# 一个包含节点友好/可读的标题的字典
|
||||
NODE_DISPLAY_NAME_MAPPINGS_V = {
|
||||
"ChatGPTOpenAI":"ChatGPT & Local LLM ♾️Mixlab",
|
||||
"ShowTextForGPT":"Show Text ♾️MixlabApp",
|
||||
"CharacterInText":"Character In Text",
|
||||
"TextSplitByDelimiter":"Text Split By Delimiter",
|
||||
}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(NODE_CLASS_MAPPINGS_V)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(NODE_DISPLAY_NAME_MAPPINGS_V)
|
||||
|
||||
except Exception as e:
|
||||
logging.info('ChatGPT.available False')
|
||||
|
||||
|
||||
try:
|
||||
from .nodes.edit_mask import EditMask
|
||||
logging.info('edit_mask.available True')
|
||||
NODE_CLASS_MAPPINGS['EditMask']=EditMask
|
||||
NODE_DISPLAY_NAME_MAPPINGS['EditMask']="Edit Mask ♾️Mixlab"
|
||||
except Exception as e:
|
||||
logging.info('edit_mask.available False')
|
||||
|
||||
try:
|
||||
from .nodes.Lama import LaMaInpainting
|
||||
logging.info('LaMaInpainting.available {}'.format(LaMaInpainting.available))
|
||||
@@ -1074,61 +1127,4 @@ try:
|
||||
except Exception as e:
|
||||
logging.info('RembgNode_.available False' )
|
||||
|
||||
|
||||
try:
|
||||
from .nodes.Video import GenerateFramesByCount,scenesNode_,CombineAudioVideo,VideoCombine_Adv,LoadVideoAndSegment,ImageListReplace,VAEEncodeForInpaint_Frames,LoadAndCombinedAudio_
|
||||
|
||||
NODE_CLASS_MAPPINGS_V = {
|
||||
"VAEEncodeForInpaint_Frames":VAEEncodeForInpaint_Frames,
|
||||
"ImageListReplace_":ImageListReplace,
|
||||
"LoadVideoAndSegment_":LoadVideoAndSegment,
|
||||
"VideoCombine_Adv":VideoCombine_Adv,
|
||||
"LoadAndCombinedAudio_":LoadAndCombinedAudio_,
|
||||
"CombineAudioVideo":CombineAudioVideo,
|
||||
"ScenesNode_":scenesNode_,
|
||||
"GenerateFramesByCount":GenerateFramesByCount
|
||||
}
|
||||
|
||||
# 一个包含节点友好/可读的标题的字典
|
||||
NODE_DISPLAY_NAME_MAPPINGS_V = {
|
||||
"VAEEncodeForInpaint_Frames":"VAE Encode For Inpaint Frames ♾️Mixlab",
|
||||
"ImageListReplace_":"Image List Replace",
|
||||
"LoadVideoAndSegment_":"Load Video And Segment",
|
||||
"VideoCombine_Adv":"Video Combine",
|
||||
"LoadAndCombinedAudio_":"Load And Combined Audio",
|
||||
"CombineAudioVideo":"Combine Audio Video",
|
||||
"ScenesNode_":"Select Scene",
|
||||
"GenerateFramesByCount":"Generate Frames By Count"
|
||||
}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(NODE_CLASS_MAPPINGS_V)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(NODE_DISPLAY_NAME_MAPPINGS_V)
|
||||
|
||||
except:
|
||||
logging.info('Video.available False')
|
||||
|
||||
|
||||
try:
|
||||
from .nodes.TripoSR import LoadTripoSRModel,TripoSRSampler,SaveTripoSRMesh
|
||||
logging.info('TripoSR.available')
|
||||
|
||||
NODE_CLASS_MAPPINGS['LoadTripoSRModel_']=LoadTripoSRModel
|
||||
NODE_DISPLAY_NAME_MAPPINGS["LoadTripoSRModel_"]= "Load TripoSR Model"
|
||||
|
||||
NODE_CLASS_MAPPINGS['TripoSRSampler_']=TripoSRSampler
|
||||
NODE_DISPLAY_NAME_MAPPINGS["TripoSRSampler_"]= "TripoSR Sampler"
|
||||
|
||||
NODE_CLASS_MAPPINGS['SaveTripoSRMesh']=SaveTripoSRMesh
|
||||
NODE_DISPLAY_NAME_MAPPINGS["SaveTripoSRMesh"]= "Save TripoSR Mesh"
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.info('TripoSR.available False' )
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
logging.info('\033[93m -------------- \033[0m')
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+505
-9216
File diff suppressed because it is too large
Load Diff
+37
-52
@@ -1,7 +1,6 @@
|
||||
|
||||
import os
|
||||
import folder_paths
|
||||
import torchaudio
|
||||
|
||||
|
||||
|
||||
class SpeechRecognition:
|
||||
@classmethod
|
||||
@@ -56,60 +55,46 @@ class SpeechSynthesis:
|
||||
return {"ui": {"text": text}, "result": (text,)}
|
||||
|
||||
|
||||
|
||||
class AudioPlayNode:
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_temp_directory()
|
||||
self.type = "temp"
|
||||
self.prefix_append = ""
|
||||
self.compress_level = 4
|
||||
#
|
||||
class GamePal:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"audio": ("AUDIO",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"input_text": ("STRING",{"multiline": True,"default": ""}),
|
||||
},
|
||||
"optional": {
|
||||
|
||||
"input_num": ("INT",{
|
||||
"default":100,
|
||||
"min": -1, #Minimum value
|
||||
"max": 0xffffffffffffffff, #Maximum value
|
||||
"step": 1, #Slider's step
|
||||
"display": "slider" # Cosmetic only: display as "number" or "slider"
|
||||
}),
|
||||
"python_code": ("STRING",{"multiline": True,"default": "result= 1 if 'Mixlab' in input_text else 0"}),
|
||||
}
|
||||
}
|
||||
|
||||
INPUT_IS_LIST = False
|
||||
RETURN_TYPES = ("INT",)
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
OUTPUT_IS_LIST = (False,)
|
||||
|
||||
CATEGORY = "♾️Mixlab/Audio"
|
||||
|
||||
INPUT_IS_LIST = False
|
||||
OUTPUT_IS_LIST = ()
|
||||
def run(self, input_text,input_num,python_code):
|
||||
exec(python_code)
|
||||
res=None
|
||||
try:
|
||||
# 可能会引发异常的代码
|
||||
res=result
|
||||
except:
|
||||
# 处理异常的代码
|
||||
print('')
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def run(self,audio):
|
||||
print(res)
|
||||
|
||||
# 判断是否是 Tensor 类型
|
||||
is_tensor = not isinstance(audio, dict)
|
||||
# print('#判断是否是 Tensor 类型',is_tensor,audio)
|
||||
if not is_tensor and 'waveform' in audio and 'sample_rate' in audio:
|
||||
# {'waveform': tensor([], size=(1, 1, 0)), 'sample_rate': 44100}
|
||||
is_tensor=True
|
||||
|
||||
if is_tensor:
|
||||
filename_prefix=""
|
||||
# 保存
|
||||
filename_prefix += self.prefix_append
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
|
||||
results = list()
|
||||
|
||||
filename_with_batch_num = filename.replace("%batch_num%", str(1))
|
||||
file = f"{filename_with_batch_num}_{counter:05}_.wav"
|
||||
|
||||
torchaudio.save(os.path.join(full_output_folder, file), audio['waveform'].squeeze(0), audio["sample_rate"])
|
||||
results.append({
|
||||
"filename": file,
|
||||
"subfolder": subfolder,
|
||||
"type": self.type
|
||||
})
|
||||
|
||||
else:
|
||||
results=[audio]
|
||||
|
||||
|
||||
# print(audio)
|
||||
return {"ui": {"audio":results}}
|
||||
# print(session_history)
|
||||
return {"ui": {"text": [input_text],"num":[input_num]}, "result": (res,)}
|
||||
+8
-21
@@ -215,28 +215,15 @@ class ChatGPTNode:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
model_list=llama_modes_list+[
|
||||
model_list=llama_modes_list+[
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-4o",
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4",
|
||||
"gpt-4-0314",
|
||||
"gpt-4-0613",
|
||||
"gpt-3.5-turbo-0301",
|
||||
"gpt-3.5-turbo-0613",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"qwen-turbo",
|
||||
"qwen-plus",
|
||||
"qwen-long",
|
||||
"qwen-max",
|
||||
"qwen-max-longcontext",
|
||||
"glm-4",
|
||||
"glm-3-turbo",
|
||||
"moonshot-v1-8k",
|
||||
"moonshot-v1-32k",
|
||||
"moonshot-v1-128k",
|
||||
"deepseek-chat"
|
||||
"gpt-3.5-turbo-0125",
|
||||
"gpt-35-turbo",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"gpt-4-0613",
|
||||
"gpt-4-1106-preview",
|
||||
"glm-4"
|
||||
]
|
||||
return {
|
||||
"required": {
|
||||
|
||||
@@ -79,7 +79,7 @@ def get_clip_interrogator_path():
|
||||
|
||||
cache_path=get_clip_interrogator_path()
|
||||
|
||||
caption_model_path=os.path.join(cache_path, "Salesforce","blip-image-captioning-base")
|
||||
caption_model_path=os.path.join(cache_path, "Salesforce/blip-image-captioning-base")
|
||||
if not os.path.exists(caption_model_path):
|
||||
print(f"## clip_interrogator_model not found: {caption_model_path}, pls download from https://huggingface.co/Salesforce/blip-image-captioning-base")
|
||||
caption_model_path='Salesforce/blip-image-captioning-base'
|
||||
|
||||
+248
-243
@@ -1,7 +1,6 @@
|
||||
import numpy as np
|
||||
import requests
|
||||
import torch
|
||||
import torchvision.transforms.v2 as T
|
||||
# from PIL import Image, ImageDraw
|
||||
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence, ImageFont
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
@@ -15,8 +14,8 @@ import cv2
|
||||
import string
|
||||
import math,glob
|
||||
from .Watcher import FolderWatcher
|
||||
import hashlib
|
||||
|
||||
from itertools import product
|
||||
|
||||
|
||||
# 将PIL图片转换为OpenCV格式
|
||||
@@ -29,102 +28,142 @@ def opencv_to_pil(image):
|
||||
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
return pil_image
|
||||
|
||||
# 列出目录下面的所有文件
|
||||
def get_files_with_extension(directory, extension):
|
||||
file_list = []
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for file in files:
|
||||
if file.endswith(extension):
|
||||
file = os.path.splitext(file)[0]
|
||||
file_path = os.path.join(root, file)
|
||||
file_name = os.path.relpath(file_path, directory)
|
||||
file_list.append(file_name)
|
||||
return file_list
|
||||
|
||||
def composite_images(foreground, background, mask, is_multiply_blend=False, position="overall", scale=0.25):
|
||||
width, height = foreground.size
|
||||
bg_image = background
|
||||
bwidth, bheight = bg_image.size
|
||||
def composite_images(foreground, background, mask,is_multiply_blend=False,position="overall"):
|
||||
width,height=foreground.size
|
||||
|
||||
bg_image=background
|
||||
|
||||
scale=max(scale,1/bwidth)
|
||||
scale=max(scale,1/bheight)
|
||||
bwidth,bheight=bg_image.size
|
||||
|
||||
def determine_scale_option(width, height):
|
||||
return 'height' if height > width else 'width'
|
||||
|
||||
if position == "overall":
|
||||
# 按z-index排序
|
||||
if position=="overall":
|
||||
layer = {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": bwidth,
|
||||
"height": bheight,
|
||||
"z_index": 88,
|
||||
"scale_option": 'overall',
|
||||
"image": foreground,
|
||||
"mask": mask
|
||||
"x":0,
|
||||
"y":0,
|
||||
"width":bwidth,
|
||||
"height":bheight,
|
||||
"z_index":88,
|
||||
"scale_option":'overall',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
else:
|
||||
scale_option = determine_scale_option(width, height)
|
||||
if scale_option == 'height':
|
||||
scale = int(bheight * scale) / height
|
||||
else:
|
||||
scale = int(bwidth * scale) / width
|
||||
|
||||
new_width = int(width * scale)
|
||||
elif position=='center_bottom':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_height = int(height * scale)
|
||||
|
||||
if position == 'center_bottom':
|
||||
x_position = int((bwidth - new_width) * 0.5)
|
||||
y_position = bheight - new_height - 24
|
||||
elif position == 'right_bottom':
|
||||
x_position = bwidth - new_width - 24
|
||||
y_position = bheight - new_height - 24
|
||||
elif position == 'center_top':
|
||||
x_position = int((bwidth - new_width) * 0.5)
|
||||
y_position = 24
|
||||
elif position == 'right_top':
|
||||
x_position = bwidth - new_width - 24
|
||||
y_position = 24
|
||||
elif position == 'left_top':
|
||||
x_position = 24
|
||||
y_position = 24
|
||||
elif position == 'left_bottom':
|
||||
x_position = 24
|
||||
y_position = bheight - new_height - 24
|
||||
elif position == 'center_center':
|
||||
x_position = int((bwidth - new_width) * 0.5)
|
||||
y_position = int((bheight - new_height) * 0.5)
|
||||
|
||||
layer = {
|
||||
"x": x_position,
|
||||
"y": y_position,
|
||||
"width": new_width,
|
||||
"height": new_height,
|
||||
"z_index": 88,
|
||||
"scale_option": scale_option,
|
||||
"image": foreground,
|
||||
"mask": mask
|
||||
"x":int(bwidth*0.75*0.5),
|
||||
"y":bheight-new_height-24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
|
||||
layer_image = layer['image']
|
||||
layer_mask = layer['mask']
|
||||
elif position=='right_bottom':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_height = int(height * scale)
|
||||
|
||||
bg_image = merge_images(bg_image,
|
||||
layer_image,
|
||||
layer_mask,
|
||||
layer['x'],
|
||||
layer['y'],
|
||||
layer['width'],
|
||||
layer['height'],
|
||||
layer['scale_option'],
|
||||
is_multiply_blend)
|
||||
layer = {
|
||||
"x":bwidth-int(bwidth*0.25)-24,
|
||||
"y":bheight-new_height-24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
|
||||
bg_image = bg_image.convert('RGB')
|
||||
|
||||
elif position=='center_top':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_height = int(height * scale)
|
||||
|
||||
layer = {
|
||||
"x":int( bwidth*0.75*0.5),
|
||||
"y":24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
|
||||
elif position=='right_top':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_height = int(height * scale)
|
||||
|
||||
layer = {
|
||||
"x":bwidth-int(bwidth*0.25)-24,
|
||||
"y":24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
elif position=='left_top':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_height = int(height * scale)
|
||||
|
||||
layer = {
|
||||
"x":24,
|
||||
"y":24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
elif position=='left_bottom':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_height = int(height * scale)
|
||||
|
||||
layer = {
|
||||
"x":24,
|
||||
"y":bheight-new_height-24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
|
||||
# width, height = bg_image.size
|
||||
|
||||
layer_image=layer['image']
|
||||
layer_mask=layer['mask']
|
||||
|
||||
bg_image=merge_images(bg_image,
|
||||
layer_image,
|
||||
layer_mask,
|
||||
layer['x'],
|
||||
layer['y'],
|
||||
layer['width'],
|
||||
layer['height'],
|
||||
layer['scale_option'],
|
||||
is_multiply_blend )
|
||||
|
||||
bg_image=bg_image.convert('RGB')
|
||||
|
||||
return bg_image
|
||||
|
||||
|
||||
|
||||
def count_files_in_directory(directory):
|
||||
file_count = 0
|
||||
for _, _, files in os.walk(directory):
|
||||
@@ -161,8 +200,7 @@ class AnyType(str):
|
||||
any_type = AnyType("*")
|
||||
|
||||
|
||||
FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),"..","assets","fonts"))
|
||||
|
||||
FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
|
||||
|
||||
MAX_RESOLUTION=8192
|
||||
|
||||
@@ -764,78 +802,85 @@ def multiply_blend(image1, image2):
|
||||
|
||||
# cv2.imwrite('result.jpg', result)
|
||||
|
||||
# 使用gpt4o优化代码
|
||||
# 为了消除图像合并时出现的灰色描边,可以使用以下方法:
|
||||
# 调整透明度:确保透明像素不会引入不需要的颜色。
|
||||
# 预处理图像:在缩放图像之前,可以先将图像的边缘进行预处理,例如扩展边缘颜色,减少抗锯齿带来的过渡效果。
|
||||
|
||||
def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option, is_multiply_blend=False):
|
||||
def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option,is_multiply_blend=False):
|
||||
# 打开底图
|
||||
bg_image = bg_image.convert("RGBA")
|
||||
|
||||
# 打开图层
|
||||
layer_image = layer_image.convert("RGBA")
|
||||
|
||||
# layer_image = layer_image.resize((width, height))
|
||||
|
||||
# 根据缩放选项调整图像大小
|
||||
if scale_option == "height":
|
||||
# 按照高度比例缩放
|
||||
original_width, original_height = layer_image.size
|
||||
scale = height / original_height
|
||||
new_width = int(original_width * scale)
|
||||
layer_image = layer_image.resize((new_width, height), Image.NEAREST)
|
||||
layer_image = layer_image.resize((new_width, height))
|
||||
elif scale_option == "width":
|
||||
# 按照宽度比例缩放
|
||||
original_width, original_height = layer_image.size
|
||||
scale = width / original_width
|
||||
new_height = int(original_height * scale)
|
||||
layer_image = layer_image.resize((width, new_height), Image.NEAREST)
|
||||
layer_image = layer_image.resize((width, new_height))
|
||||
elif scale_option == "overall":
|
||||
# 整体缩放
|
||||
layer_image = layer_image.resize((width, height), Image.NEAREST)
|
||||
layer_image = layer_image.resize((width, height))
|
||||
|
||||
elif scale_option == "longest":
|
||||
original_width, original_height = layer_image.size
|
||||
if original_width > original_height:
|
||||
new_width = width
|
||||
new_width=width
|
||||
scale = width / original_width
|
||||
new_height = int(original_height * scale)
|
||||
x = 0
|
||||
y = int((height - new_height) * 0.5)
|
||||
x=0
|
||||
y=int((height-new_height)*0.5)
|
||||
else:
|
||||
new_height = height
|
||||
new_height=height
|
||||
scale = height / original_height
|
||||
new_width = int(original_height * scale)
|
||||
x = int((width - new_width) * 0.5)
|
||||
y = 0
|
||||
x=int((width-new_width)*0.5)
|
||||
y=0
|
||||
# elif side == "shortest":
|
||||
# if width < height:
|
||||
#
|
||||
# else:
|
||||
#
|
||||
|
||||
|
||||
# 调整mask的大小
|
||||
nw, nh = layer_image.size
|
||||
mask = mask.resize((nw, nh), Image.NEAREST)
|
||||
mask = mask.resize((nw, nh))
|
||||
|
||||
# 预处理图像边缘以减少灰色描边
|
||||
layer_image = layer_image.filter(ImageFilter.SMOOTH)
|
||||
# # 分离出a通道
|
||||
# r, g, b, alpha = layer_image.split()
|
||||
# alpha = ImageOps.invert(alpha)
|
||||
# # 创建一个新的RGB图像
|
||||
# new_rgb_image = Image.new("RGB", layer_image.size)
|
||||
# # 将透明通道粘贴到新的RGB图像上
|
||||
# new_rgb_image.paste(layer_image, (0, 0), mask=alpha)
|
||||
|
||||
# new_rgb_image.paste(layer_image, (x, y), mask=mask)
|
||||
# mask=new_rgb_image.convert('L')
|
||||
# mask = ImageOps.invert(mask)
|
||||
|
||||
if is_multiply_blend:
|
||||
bg_image_white = Image.new("RGB", bg_image.size, (255, 255, 255))
|
||||
bg_image_white=Image.new("RGB", bg_image.size,(255, 255, 255))
|
||||
|
||||
bg_image_white.paste(layer_image, (x, y), mask=mask)
|
||||
bg_image = multiply_blend(bg_image_white, bg_image)
|
||||
bg_image = bg_image.convert("RGBA")
|
||||
bg_image=multiply_blend(bg_image_white,bg_image)
|
||||
bg_image=bg_image.convert("RGBA")
|
||||
else:
|
||||
transparent_img = Image.new("RGBA", layer_image.size, (255, 255, 255, 0))
|
||||
# 调整透明度处理
|
||||
for i in range(transparent_img.size[0]):
|
||||
for j in range(transparent_img.size[1]):
|
||||
r, g, b, a = transparent_img.getpixel((i, j))
|
||||
if a > 0:
|
||||
transparent_img.putpixel((i, j), (r, g, b, 255))
|
||||
|
||||
transparent_img.paste(layer_image, (0, 0), mask)
|
||||
transparent_img = Image.new("RGBA",layer_image.size, (255, 255, 255, 0))
|
||||
transparent_img.paste(layer_image,(0, 0), mask)
|
||||
# transparent_img.save('test.png')
|
||||
bg_image.paste(transparent_img, (x, y), transparent_img)
|
||||
|
||||
|
||||
# 输出合成后的图片
|
||||
return bg_image
|
||||
|
||||
#MixCopilot
|
||||
|
||||
def resize_2(img):
|
||||
# 检查图像的高度是否是2的倍数,如果不是,则调整高度
|
||||
@@ -909,13 +954,53 @@ def resize_image(layer_image, scale_option, width, height,color="white"):
|
||||
return layer_image
|
||||
|
||||
|
||||
def generate_text_image(text, font_path, font_size, text_color, vertical=True, stroke=False, stroke_color=(0, 0, 0), stroke_width=1, spacing=0, padding=4):
|
||||
|
||||
|
||||
|
||||
|
||||
# def generate_text_image(text_list, font_path, font_size, text_color, vertical=True, spacing=0):
|
||||
# # Load Chinese font
|
||||
# font = ImageFont.truetype(font_path, font_size)
|
||||
|
||||
# # Calculate image size based on the number of characters and orientation
|
||||
# if vertical:
|
||||
# width = font_size + 100
|
||||
# height = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
|
||||
# else:
|
||||
# width = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
|
||||
# height = font_size + 100
|
||||
|
||||
# # Create a blank image
|
||||
# image = Image.new('RGBA', (width, height), (255, 255, 255,0))
|
||||
# draw = ImageDraw.Draw(image)
|
||||
|
||||
# # Draw text
|
||||
# if vertical:
|
||||
# for i, char in enumerate(text_list):
|
||||
# char_position = (50, 50 + i * font_size)
|
||||
# draw.text(char_position, char, font=font, fill=text_color)
|
||||
# else:
|
||||
# for i, char in enumerate(text_list):
|
||||
# char_position = (50 + i * (font_size + spacing), 50)
|
||||
# draw.text(char_position, char, font=font, fill=text_color)
|
||||
|
||||
# # Save the image
|
||||
# # image.save(output_image_path)
|
||||
|
||||
# # 分离alpha通道
|
||||
# alpha_channel = image.split()[3]
|
||||
|
||||
# # 创建一个只有alpha通道的新图像
|
||||
# alpha_image = Image.new('L', image.size)
|
||||
# alpha_image.putdata(alpha_channel.getdata())
|
||||
|
||||
# image=image.convert('RGB')
|
||||
|
||||
# return (image,alpha_image)
|
||||
def generate_text_image(text, font_path, font_size, text_color, vertical=True, stroke=False, stroke_color=(0, 0, 0), stroke_width=1, spacing=0):
|
||||
# Split text into lines based on line breaks
|
||||
lines = text.split("\n")
|
||||
|
||||
# Load font
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
|
||||
# 1. Determine layout direction
|
||||
if vertical:
|
||||
layout = "vertical"
|
||||
@@ -924,46 +1009,49 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
|
||||
|
||||
# 2. Calculate absolute coordinates for each character
|
||||
char_coordinates = []
|
||||
x, y = padding, padding
|
||||
max_width, max_height = 0, 0
|
||||
|
||||
if layout == "vertical":
|
||||
for line in lines:
|
||||
max_char_width = max(font.getsize(char)[0] for char in line)
|
||||
x = 0
|
||||
y = 0
|
||||
for i in range(len(lines)):
|
||||
line = lines[i]
|
||||
for char in line:
|
||||
char_width, char_height = font.getsize(char)
|
||||
char_coordinates.append((x, y))
|
||||
y += char_height + spacing
|
||||
max_height = max(max_height, y + padding)
|
||||
x += max_char_width + spacing
|
||||
y = padding
|
||||
max_width = x
|
||||
y += font_size + spacing
|
||||
x += font_size + spacing
|
||||
y = 0
|
||||
else:
|
||||
x = 0
|
||||
y = 0
|
||||
for line in lines:
|
||||
line_width, line_height = font.getsize(line)
|
||||
for char in line:
|
||||
char_width, char_height = font.getsize(char)
|
||||
char_coordinates.append((x, y))
|
||||
x += char_width + spacing
|
||||
max_width = max(max_width, x + padding)
|
||||
y += line_height + spacing
|
||||
x = padding
|
||||
max_height = y
|
||||
x += font_size + spacing
|
||||
y += font_size + spacing
|
||||
x = 0
|
||||
|
||||
# 3. Create image with calculated width and height
|
||||
image = Image.new('RGBA', (max_width, max_height), (255, 255, 255, 0))
|
||||
draw = ImageDraw.Draw(image)
|
||||
# 3. Calculate image width and height
|
||||
if layout == "vertical":
|
||||
width = (len(lines) * (font_size + spacing)) - spacing
|
||||
height = ((len(max(lines, key=len)) + 1) * (font_size + spacing)) + spacing
|
||||
else:
|
||||
width = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
|
||||
height = ((len(lines) - 1) * (font_size + spacing)) + font_size
|
||||
|
||||
# 4. Draw each character on the image
|
||||
image = Image.new('RGBA', (width, height), (255, 255, 255, 0))
|
||||
draw = ImageDraw.Draw(image)
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
|
||||
index = 0
|
||||
for line in lines:
|
||||
for char in line:
|
||||
for i, line in enumerate(lines):
|
||||
for j, char in enumerate(line):
|
||||
x, y = char_coordinates[index]
|
||||
|
||||
if stroke:
|
||||
draw.text((x-stroke_width, y), char, font=font, fill=text_color)
|
||||
draw.text((x+stroke_width, y), char, font=font, fill=text_color)
|
||||
draw.text((x, y-stroke_width), char, font=font, fill=text_color)
|
||||
draw.text((x, y+stroke_width), char, font=font, fill=text_color)
|
||||
draw.text((x-stroke_width, y), char, font=font, fill=stroke_color)
|
||||
draw.text((x+stroke_width, y), char, font=font, fill=stroke_color)
|
||||
draw.text((x, y-stroke_width), char, font=font, fill=stroke_color)
|
||||
draw.text((x, y+stroke_width), char, font=font, fill=stroke_color)
|
||||
|
||||
draw.text((x, y), char, font=font, fill=text_color)
|
||||
index += 1
|
||||
@@ -1490,7 +1578,7 @@ class ImageCropByAlpha:
|
||||
|
||||
|
||||
|
||||
# get_files_with_extension(FONT_PATH,'.ttf')
|
||||
|
||||
|
||||
class TextImage:
|
||||
@classmethod
|
||||
@@ -1498,7 +1586,7 @@ class TextImage:
|
||||
return {"required": {
|
||||
|
||||
"text": ("STRING",{"multiline": True,"default": "龍馬精神迎新歲","dynamicPrompts": False}),
|
||||
"font": (get_files_with_extension(FONT_PATH,'.ttf'),),#后缀为 ttf
|
||||
"font_path": ("STRING",{"multiline": False,"default": FONT_PATH,"dynamicPrompts": False}),
|
||||
"font_size": ("INT",{
|
||||
"default":100,
|
||||
"min": 100, #Minimum value
|
||||
@@ -1513,13 +1601,6 @@ class TextImage:
|
||||
"step": 1, #Slider's step
|
||||
"display": "number" # Cosmetic only: display as "number" or "slider"
|
||||
}),
|
||||
"padding": ("INT",{
|
||||
"default":8,
|
||||
"min": 0, #Minimum value
|
||||
"max": 200, #Maximum value
|
||||
"step": 1, #Slider's step
|
||||
"display": "number" # Cosmetic only: display as "number" or "slider"
|
||||
}),
|
||||
"text_color":("STRING",{"multiline": False,"default": "#000000","dynamicPrompts": False}),
|
||||
"vertical":("BOOLEAN", {"default": True},),
|
||||
"stroke":("BOOLEAN", {"default": False},),
|
||||
@@ -1527,7 +1608,7 @@ class TextImage:
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE","MASK",)
|
||||
RETURN_NAMES = ("image","mask",)
|
||||
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
@@ -1536,14 +1617,11 @@ class TextImage:
|
||||
INPUT_IS_LIST = False
|
||||
OUTPUT_IS_LIST = (False,False,)
|
||||
|
||||
def run(self,text,font,font_size,spacing,padding,text_color,vertical,stroke):
|
||||
def run(self,text,font_path,font_size,spacing,text_color,vertical,stroke):
|
||||
|
||||
font_path=os.path.join(FONT_PATH,font+'.ttf')
|
||||
|
||||
if text=="":
|
||||
text=" "
|
||||
# text_list=list(text)
|
||||
# stroke=False, stroke_color=(0, 0, 0), stroke_width=1, spacing=0
|
||||
img,mask=generate_text_image(text,font_path,font_size,text_color,vertical,stroke,(0, 0, 0),1,spacing,padding)
|
||||
img,mask=generate_text_image(text,font_path,font_size,text_color,vertical,stroke,(0, 0, 0),1,spacing)
|
||||
|
||||
img=pil2tensor(img)
|
||||
mask=pil2tensor(mask)
|
||||
@@ -1776,16 +1854,10 @@ class CompositeImages:
|
||||
"mask":("MASK",),
|
||||
"background": ("IMAGE",),
|
||||
},
|
||||
"optional":{
|
||||
"optional":{
|
||||
|
||||
"is_multiply_blend": ("BOOLEAN", {"default": False}),
|
||||
"position": (['overall',"center_center","left_bottom","center_bottom","right_bottom","left_top","center_top","right_top"],),
|
||||
"scale": ("FLOAT",{
|
||||
"default":0.35,
|
||||
"min": 0.01, #Minimum value
|
||||
"max": 1, #Maximum value
|
||||
"step": 0.01, #Slider's step
|
||||
"display": "number" # Cosmetic only: display as "number" or "slider"
|
||||
}),
|
||||
"position": (['overall',"center_bottom","center_top","right_bottom","left_bottom","right_top","left_top"],),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1798,33 +1870,15 @@ class CompositeImages:
|
||||
|
||||
# OUTPUT_IS_LIST = (True,)
|
||||
|
||||
# def run(self, foreground,mask,background,is_multiply_blend,position,scale):
|
||||
# foreground= tensor2pil(foreground)
|
||||
# mask= tensor2pil(mask)
|
||||
# background= tensor2pil(background)
|
||||
# res=composite_images(foreground,background,mask,is_multiply_blend,position,scale)
|
||||
def run(self, foreground,mask,background,is_multiply_blend,position):
|
||||
foreground= tensor2pil(foreground)
|
||||
mask= tensor2pil(mask)
|
||||
background= tensor2pil(background)
|
||||
res=composite_images(foreground,background,mask,is_multiply_blend,position)
|
||||
|
||||
# return (pil2tensor(res),)
|
||||
return (pil2tensor(res),)
|
||||
|
||||
def run(self, foreground,mask,background, is_multiply_blend, position, scale):
|
||||
results = []
|
||||
|
||||
f1=[]
|
||||
for fg, mask in zip(foreground, mask ):
|
||||
f1.append([fg,mask])
|
||||
|
||||
|
||||
for f, bg in product(f1, background):
|
||||
[fg,mask]=f
|
||||
fg_pil = tensor2pil(fg)
|
||||
mask_pil = tensor2pil(mask)
|
||||
bg_pil = tensor2pil(bg)
|
||||
res = composite_images(fg_pil, bg_pil, mask_pil, is_multiply_blend, position, scale)
|
||||
results.append(pil2tensor(res))
|
||||
|
||||
output_image = torch.cat(results, dim=0)
|
||||
|
||||
return (output_image,)
|
||||
|
||||
|
||||
class EmptyLayer:
|
||||
@@ -3153,52 +3207,3 @@ class SaveImageToLocal:
|
||||
counter += 1
|
||||
|
||||
return ()
|
||||
|
||||
|
||||
class ImageBatchToList_:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {"image_batch": ("IMAGE",), }}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image_list",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/Image"
|
||||
|
||||
def run(self, image_batch):
|
||||
images = [image_batch[i:i + 1, ...] for i in range(image_batch.shape[0])]
|
||||
return (images, )
|
||||
|
||||
class ImageListToBatch_:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "run"
|
||||
INPUT_IS_LIST = True
|
||||
CATEGORY = "♾️Mixlab/Image"
|
||||
|
||||
def run(self, images):
|
||||
shape = images[0].shape[1:3]
|
||||
out = []
|
||||
|
||||
for i in range(len(images)):
|
||||
img = images[i].permute([0,3,1,2])
|
||||
if images[i].shape[1:3] != shape:
|
||||
transforms = T.Compose([
|
||||
T.CenterCrop(min(img.shape[2], img.shape[3])),
|
||||
T.Resize((shape[0], shape[1]), interpolation=T.InterpolationMode.BICUBIC),
|
||||
])
|
||||
img = transforms(img)
|
||||
out.append(img.permute([0,2,3,1]))
|
||||
|
||||
out = torch.cat(out, dim=0)
|
||||
|
||||
return (out,)
|
||||
@@ -85,6 +85,8 @@ class LaMaInpainting:
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
|
||||
@@ -90,7 +90,7 @@ class ScreenShareNode:
|
||||
} }
|
||||
|
||||
RETURN_TYPES = ('IMAGE','STRING','FLOAT',"INT")
|
||||
RETURN_NAMES = ("current frame (image)","prompt","denoise (float)","seed (int)")
|
||||
RETURN_NAMES = ("IMAGE","PROMPT","FLOAT","INT")
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/Screen"
|
||||
@@ -109,7 +109,7 @@ class FloatingVideo:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return { "required":{
|
||||
"image": ("IMAGE",)
|
||||
"images": ("IMAGE",)
|
||||
}, }
|
||||
|
||||
# RETURN_TYPES = ('IMAGE','MASK')
|
||||
@@ -124,16 +124,16 @@ class FloatingVideo:
|
||||
# OUTPUT_IS_LIST = (False,False,)
|
||||
|
||||
# 运行的函数
|
||||
def run(self,image):
|
||||
def run(self,images):
|
||||
|
||||
results = list()
|
||||
|
||||
for im in image:
|
||||
im=tensor2pil(im)
|
||||
for image in images:
|
||||
image=tensor2pil(image)
|
||||
# image_base64 = base64.b64encode(image.tobytes())
|
||||
|
||||
buffered = BytesIO()
|
||||
im.save(buffered, format="JPEG")
|
||||
image.save(buffered, format="JPEG")
|
||||
image_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
|
||||
results.append(image_base64)
|
||||
|
||||
+5
-5
@@ -133,7 +133,7 @@ def get_font_files(directory):
|
||||
|
||||
return font_files
|
||||
|
||||
r_directory = os.path.join(os.path.dirname(__file__), '..','assets','/')
|
||||
r_directory = os.path.join(os.path.dirname(__file__), '../assets/')
|
||||
|
||||
font_files = get_font_files(r_directory)
|
||||
# print(font_files)
|
||||
@@ -566,7 +566,7 @@ class AppInfo:
|
||||
},
|
||||
|
||||
"optional":{
|
||||
"image": ("IMAGE",),
|
||||
"IMAGE": ("IMAGE",),
|
||||
"description":("STRING",{"multiline": True,"default": "","dynamicPrompts": False}),
|
||||
"version":("INT", {
|
||||
"default": 1,
|
||||
@@ -594,12 +594,12 @@ class AppInfo:
|
||||
INPUT_IS_LIST = True
|
||||
# OUTPUT_IS_LIST = (True,)
|
||||
|
||||
def run(self,name,input_ids,output_ids,image,description,version,share_prefix,link,category,auto_save):
|
||||
def run(self,name,input_ids,output_ids,IMAGE,description,version,share_prefix,link,category,auto_save):
|
||||
name=name[0]
|
||||
|
||||
im=None
|
||||
if image:
|
||||
im=image[0][0]
|
||||
if IMAGE:
|
||||
im=IMAGE[0][0]
|
||||
#TODO batch 的方式需要处理
|
||||
im=create_temp_file(im)
|
||||
# image [img,] img[batch,w,h,a] 列表里面是batch,
|
||||
|
||||
+69
-349
@@ -17,128 +17,9 @@ import folder_paths
|
||||
from comfy.k_diffusion.utils import FolderOfImages
|
||||
from comfy.utils import common_upscale
|
||||
|
||||
import torchaudio
|
||||
import base64
|
||||
|
||||
import mimetypes
|
||||
|
||||
|
||||
|
||||
def get_frames(frame_count, frames, revert=False):
|
||||
if not revert:
|
||||
if frame_count <= len(frames):
|
||||
return frames[:frame_count]
|
||||
else:
|
||||
return [frames[i % len(frames)] for i in range(frame_count)]
|
||||
else:
|
||||
extended_frames = frames + frames[-2:0:-1] # 正向加反向中间部分
|
||||
if frame_count <= len(extended_frames):
|
||||
return extended_frames[:frame_count]
|
||||
else:
|
||||
return [extended_frames[i % len(extended_frames)] for i in range(frame_count)]
|
||||
|
||||
# # 示例用法
|
||||
# frames = ["frame1", "frame2", "frame3"]
|
||||
# frame_count = 2
|
||||
|
||||
# result = get_frames(frame_count, frames, revert=False)
|
||||
# print(result) # 输出: ['frame1', 'frame2', 'frame3', 'frame1', 'frame2', 'frame3', 'frame1']
|
||||
|
||||
# result = get_frames(frame_count, frames, revert=True)
|
||||
# print(result) # 输出: ['frame1', 'frame2', 'frame3', 'frame2', 'frame1', 'frame2', 'frame3']
|
||||
|
||||
|
||||
|
||||
|
||||
def get_mime_type(file_path):
|
||||
# 获取文件的 MIME 类型
|
||||
mime_type, _ = mimetypes.guess_type(file_path)
|
||||
|
||||
# 如果无法猜测类型,返回默认类型
|
||||
if mime_type is None:
|
||||
return 'application/octet-stream'
|
||||
|
||||
return mime_type
|
||||
# import subprocess
|
||||
# from imageio_ffmpeg import get_ffmpeg_exe
|
||||
|
||||
|
||||
def save_audio_base64s_to_file(base64_audios, output_folder, file_name):
|
||||
# Ensure the output folder exists
|
||||
if not os.path.exists(output_folder):
|
||||
os.makedirs(output_folder)
|
||||
|
||||
decoded_audios=[]
|
||||
for a in base64_audios:
|
||||
|
||||
# If the base64 string contains a header, remove it
|
||||
if ',' in a:
|
||||
a = a.split(',')[1]
|
||||
|
||||
# 解码 base64 数据
|
||||
a=base64.b64decode(a)
|
||||
decoded_audios.append(a)
|
||||
|
||||
# 拼接音频数据
|
||||
combined_audio = b''.join(decoded_audios)
|
||||
|
||||
# Create the full file path
|
||||
file_path = os.path.join(output_folder, file_name)
|
||||
|
||||
# Write the decoded audio to the file
|
||||
with open(file_path, 'wb') as audio_file:
|
||||
audio_file.write(combined_audio)
|
||||
|
||||
return file_path
|
||||
|
||||
# Example usage
|
||||
# base64_audio = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAIlYAAESsAAACABAAZGF0YQAAAAA="
|
||||
# output_folder = "audio_files"
|
||||
# file_name = "output.wav"
|
||||
|
||||
# file_path = save_audio_base64_to_file(base64_audio, output_folder, file_name)
|
||||
# print(f"Audio saved to: {file_path}")
|
||||
|
||||
# 写一个python文件,用来 判断文件夹内命名为 所有chat_tts开头的文件数量(chat_tts_00001),并输出新的编号
|
||||
def get_new_counter(full_output_folder, filename_prefix):
|
||||
# 获取目录中的所有文件
|
||||
files = os.listdir(full_output_folder)
|
||||
|
||||
# 过滤出以 filename_prefix 开头并且后续部分为数字的文件
|
||||
filtered_files = []
|
||||
for f in files:
|
||||
if f.startswith(filename_prefix):
|
||||
# 去掉文件名中的前缀和后缀,只保留中间的数字部分
|
||||
base_name = f[len(filename_prefix)+1:]
|
||||
number_part = base_name.split('.')[0] # 假设文件名中只有一个点,即扩展名
|
||||
if number_part.isdigit():
|
||||
filtered_files.append(int(number_part))
|
||||
|
||||
if not filtered_files:
|
||||
return 1
|
||||
|
||||
# 获取最大的编号
|
||||
max_number = max(filtered_files)
|
||||
|
||||
# 新的编号
|
||||
return max_number + 1
|
||||
|
||||
def crop_audio(input_file, start_time, duration):
|
||||
# Load the audio file
|
||||
audio_tensor, sample_rate = torchaudio.load(input_file)
|
||||
|
||||
# Convert start_time and duration from seconds to sample indices
|
||||
start_sample = int(start_time * sample_rate)
|
||||
end_sample = start_sample + int(duration * sample_rate)
|
||||
|
||||
# Perform the slicing
|
||||
cropped_audio_tensor = audio_tensor[:, start_sample:end_sample]
|
||||
|
||||
# Save the cropped audio to a new file
|
||||
torchaudio.save(input_file, cropped_audio_tensor, sample_rate)
|
||||
|
||||
return input_file
|
||||
|
||||
def generate_folder_name(directory,video_path):
|
||||
# Get the directory and filename from the video path
|
||||
_, filename = os.path.split(video_path)
|
||||
@@ -179,9 +60,6 @@ def split_video(video_path, video_segment_frames, transition_frames, output_dir)
|
||||
|
||||
# 打印当前片段的起始帧和结束帧
|
||||
print(f"Segment {i+1}: Start Frame {start_frame}, End Frame {end_frame}")
|
||||
|
||||
if end_frame<start_frame:
|
||||
break
|
||||
|
||||
# 保存当前片段为一个视频文件
|
||||
segment_video_path = f"{output_dir}/segment_{i+1}.avi"
|
||||
@@ -190,7 +68,6 @@ def split_video(video_path, video_segment_frames, transition_frames, output_dir)
|
||||
segment_video = cv2.VideoWriter(segment_video_path, fourcc, fps, (int(video_capture.get(cv2.CAP_PROP_FRAME_WIDTH)),
|
||||
int(video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT))))
|
||||
|
||||
|
||||
for frame_num in range(start_frame, end_frame):
|
||||
ret, frame = video_capture.read()
|
||||
if ret:
|
||||
@@ -224,25 +101,6 @@ if ffmpeg_path is None:
|
||||
except:
|
||||
print("ffmpeg could not be found. Outputs that require it have been disabled")
|
||||
|
||||
|
||||
def combine_audio_video(audio_path, video_path, output_path):
|
||||
|
||||
command = [
|
||||
ffmpeg_path,
|
||||
'-i', video_path,
|
||||
'-i', audio_path,
|
||||
'-c:v', 'copy',
|
||||
'-c:a', 'aac',
|
||||
'-shortest',
|
||||
output_path
|
||||
]
|
||||
|
||||
subprocess.run(command, check=True)
|
||||
return output_path
|
||||
|
||||
|
||||
|
||||
|
||||
# Tensor to PIL
|
||||
def tensor2pil(image):
|
||||
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
@@ -404,7 +262,7 @@ class LoadVideoAndSegment:
|
||||
files.append(f)
|
||||
return {"required": {
|
||||
"video": (sorted(files), {"video_upload": True}),
|
||||
"video_segment_frames": ("INT", {"default": 10, "min": -1, "step": 1}),
|
||||
"video_segment_frames": ("INT", {"default": 10, "min": 1, "step": 1}),
|
||||
"transition_frames": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
},}
|
||||
|
||||
@@ -474,6 +332,63 @@ class LoadVideoAndSegment:
|
||||
|
||||
video_path = folder_paths.get_annotated_filepath(video)
|
||||
|
||||
# check if video is a gif - will need to use cv fallback to read frames
|
||||
# use cv fallback if ffmpeg not installed or gif
|
||||
# if ffmpeg_path is None:
|
||||
# return self.load_video_cv_fallback(video, frame_load_cap, skip_first_frames)
|
||||
# otherwise, continue with ffmpeg
|
||||
|
||||
# args_dummy = [ffmpeg_path, "-i", video_path, "-f", "null", "-"]
|
||||
# try:
|
||||
# with subprocess.Popen(args_dummy, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) as proc:
|
||||
# for line in proc.stderr.readlines():
|
||||
# match = re.search(", ([1-9]|\\d{2,})x(\\d+)",line.decode('utf-8'))
|
||||
# if match is not None:
|
||||
# size = [int(match.group(1)), int(match.group(2))]
|
||||
# break
|
||||
# except Exception as e:
|
||||
# print(f"Retrying with opencv due to ffmpeg error: {e}")
|
||||
# return self.load_video_cv_fallback(video, frame_load_cap, skip_first_frames)
|
||||
# args_all_frames = [ffmpeg_path, "-i", video_path, "-v", "error",
|
||||
# "-pix_fmt", "rgb24"]
|
||||
|
||||
# vfilters = []
|
||||
|
||||
# if skip_first_frames > 0:
|
||||
# vfilters.append(f"select=gt(n\\,{skip_first_frames-1})")
|
||||
# if frame_load_cap > 0:
|
||||
# vfilters.append(f"select=gt({frame_load_cap}\\,n)")
|
||||
# #manually calculate aspect ratio to ensure reads remain aligned
|
||||
|
||||
# if len(vfilters) > 0:
|
||||
# args_all_frames += ["-vf", ",".join(vfilters)]
|
||||
|
||||
# args_all_frames += ["-f", "rawvideo", "-"]
|
||||
# images = []
|
||||
# try:
|
||||
# with subprocess.Popen(args_all_frames, stdout=subprocess.PIPE) as proc:
|
||||
# #Manually buffer enough bytes for an image
|
||||
# bpi = size[0]*size[1]*3
|
||||
# current_bytes = bytearray(bpi)
|
||||
# current_offset=0
|
||||
# while True:
|
||||
# bytes_read = proc.stdout.read(bpi - current_offset)
|
||||
# if bytes_read is None:#sleep to wait for more data
|
||||
# time.sleep(.2)
|
||||
# continue
|
||||
# if len(bytes_read) == 0:#EOF
|
||||
# break
|
||||
# current_bytes[current_offset:len(bytes_read)] = bytes_read
|
||||
# current_offset+=len(bytes_read)
|
||||
# if current_offset == bpi:
|
||||
# images.append(np.array(current_bytes, dtype=np.float32).reshape(size[1], size[0], 3) / 255.0)
|
||||
# current_offset = 0
|
||||
# except Exception as e:
|
||||
# print(f"Retrying with opencv due to ffmpeg error: {e}")
|
||||
# return self.load_video_cv_fallback(video, frame_load_cap, skip_first_frames)
|
||||
|
||||
# imgs=split_list(images,video_segment_frames,transition_frames)
|
||||
|
||||
# temp path
|
||||
tp=folder_paths.get_temp_directory()
|
||||
basename = os.path.basename(video_path) # 获取文件名
|
||||
@@ -481,22 +396,15 @@ class LoadVideoAndSegment:
|
||||
|
||||
folder_path = create_folder(tp,name_without_extension)
|
||||
|
||||
if video_segment_frames==-1:
|
||||
# 不切割视频
|
||||
scenes_video=[video_path]
|
||||
# 读取视频文件
|
||||
video_capture = cv2.VideoCapture(video_path)
|
||||
|
||||
# 获取视频的总帧数和帧率
|
||||
total_frames = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
fps = video_capture.get(cv2.CAP_PROP_FPS)
|
||||
|
||||
else:
|
||||
# 导出的数据
|
||||
scenes_video,total_frames,fps=split_video(video_path,video_segment_frames,
|
||||
transition_frames,folder_path)
|
||||
|
||||
# 导出的数据
|
||||
scenes_video,total_frames,fps=split_video(video_path,video_segment_frames,
|
||||
transition_frames,folder_path)
|
||||
|
||||
|
||||
# imgs=[torch.from_numpy(np.stack(im)) for im in imgs]
|
||||
|
||||
# images = torch.from_numpy(np.stack(images))
|
||||
|
||||
return (scenes_video,len(scenes_video), total_frames,fps,)
|
||||
|
||||
@@ -514,85 +422,7 @@ class LoadVideoAndSegment:
|
||||
return "Invalid image file: {}".format(video)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
class LoadAndCombinedAudio_:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
|
||||
return {"required": {
|
||||
"audios": ("AUDIOBASE64",),
|
||||
"start_time": ("FLOAT" , {"default": 0, "min": 0, "max": 10000000, "step": 0.01}),
|
||||
"duration": ("FLOAT" , {"default": 10, "min": -1, "max": 10000000, "step": 0.01}),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "♾️Mixlab/Audio"
|
||||
|
||||
RETURN_TYPES = ("STRING","AUDIO",)
|
||||
RETURN_NAMES = ("audio_file_path","audio",)
|
||||
FUNCTION = "run"
|
||||
|
||||
def run(self,audios, start_time, duration):
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
counter=get_new_counter(output_dir,'audio_')
|
||||
|
||||
audio_file_name = f"audio_{counter:05}.wav"
|
||||
|
||||
audio_file=save_audio_base64s_to_file(audios['base64'],output_dir,audio_file_name)
|
||||
# duration == -1 则不裁切
|
||||
if duration > -1:
|
||||
crop_audio(audio_file, start_time, duration)
|
||||
|
||||
return (audio_file, {
|
||||
"filename": audio_file_name,
|
||||
"subfolder": "",
|
||||
"type": "output",
|
||||
"audio_path":audio_file
|
||||
} ,)
|
||||
|
||||
class CombineAudioVideo:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
|
||||
return {"required": {
|
||||
"video_file_path": ("STRING", {"forceInput": True}),
|
||||
"audio_file_path": ("STRING", {"forceInput": True}),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "run"
|
||||
RETURN_TYPES = ()
|
||||
RETURN_NAMES = ()
|
||||
|
||||
def run(self,video_file_path, audio_file_path):
|
||||
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
|
||||
counter=get_new_counter(output_dir,'video_final_')
|
||||
|
||||
# 获取文件名和扩展名
|
||||
base, ext = os.path.splitext(video_file_path)
|
||||
|
||||
v_file = f"video_final_{counter:05}{ext}"
|
||||
|
||||
v_file_path=os.path.join(output_dir, v_file)
|
||||
|
||||
combine_audio_video(audio_file_path,video_file_path,v_file_path)
|
||||
|
||||
previews = [
|
||||
{
|
||||
"filename": v_file,
|
||||
"subfolder": "",
|
||||
"type": "output",
|
||||
"format": get_mime_type(v_file),
|
||||
}
|
||||
]
|
||||
return {"ui": {"gifs": previews}}
|
||||
|
||||
|
||||
# The code is based on ComfyUI-VideoHelperSuite modification.
|
||||
class VideoCombine_Adv:
|
||||
@@ -624,8 +454,7 @@ class VideoCombine_Adv:
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SCENE_VIDEO",)
|
||||
RETURN_NAMES = ("scenes_video",)
|
||||
RETURN_TYPES = ()
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
FUNCTION = "run"
|
||||
@@ -794,7 +623,7 @@ class VideoCombine_Adv:
|
||||
"format": format,
|
||||
}
|
||||
]
|
||||
return {"ui": {"gifs": previews},"result":(file_path,)}
|
||||
return {"ui": {"gifs": previews}}
|
||||
|
||||
|
||||
class VAEEncodeForInpaint_Frames:
|
||||
@@ -861,113 +690,4 @@ class VAEEncodeForInpaint_Frames:
|
||||
result.append({"samples":t, "noise_mask": (mask_erosion[:,:,:x,:y].round())})
|
||||
|
||||
|
||||
return (result, )
|
||||
|
||||
|
||||
|
||||
class GenerateFramesByCount:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
return {"required": {
|
||||
"frames": ('IMAGE',),
|
||||
"frame_count": ("INT", {"default": 72, "min": 1, "step": 1}),
|
||||
"revert" :("BOOLEAN", {"default": True},),
|
||||
},}
|
||||
|
||||
RETURN_TYPES = ('IMAGE',)
|
||||
RETURN_NAMES = ("frames",)
|
||||
|
||||
FUNCTION = "r"
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
# INPUT_IS_LIST = True
|
||||
|
||||
def r(self, frames, frame_count, revert):
|
||||
|
||||
image_list = [frames[i:i + 1, ...] for i in range(frames.shape[0])]
|
||||
|
||||
image_list=get_frames(frame_count,image_list,revert)
|
||||
|
||||
images = torch.cat(image_list, dim=0)
|
||||
|
||||
return (images,)
|
||||
|
||||
|
||||
class scenesNode_:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
return {"required": {
|
||||
"scenes_video": ('SCENE_VIDEO',),
|
||||
"index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
|
||||
},}
|
||||
|
||||
RETURN_TYPES = ('IMAGE','INT',)
|
||||
RETURN_NAMES = ("video frames (batch)","count",)
|
||||
# OUTPUT_IS_LIST = (False,)
|
||||
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
INPUT_IS_LIST = True
|
||||
|
||||
def load_video_cv_fallback(self, video, frame_load_cap, skip_first_frames):
|
||||
# print('#video',video)
|
||||
try:
|
||||
video_cap = cv2.VideoCapture(video)
|
||||
if not video_cap.isOpened():
|
||||
raise ValueError(f"{video} could not be loaded with cv fallback.")
|
||||
# set video_cap to look at start_index frame
|
||||
images = []
|
||||
total_frame_count = 0
|
||||
frames_added = 0
|
||||
base_frame_time = 1/video_cap.get(cv2.CAP_PROP_FPS)
|
||||
|
||||
target_frame_time = base_frame_time
|
||||
|
||||
time_offset=0.0
|
||||
while video_cap.isOpened():
|
||||
if time_offset < target_frame_time:
|
||||
is_returned, frame = video_cap.read()
|
||||
# if didn't return frame, video has ended
|
||||
if not is_returned:
|
||||
break
|
||||
time_offset += base_frame_time
|
||||
if time_offset < target_frame_time:
|
||||
continue
|
||||
time_offset -= target_frame_time
|
||||
# if not at start_index, skip doing anything with frame
|
||||
total_frame_count += 1
|
||||
if total_frame_count <= skip_first_frames:
|
||||
continue
|
||||
# TODO: do whatever operations need to happen, like force_size, etc
|
||||
|
||||
# opencv loads images in BGR format (yuck), so need to convert to RGB for ComfyUI use
|
||||
# follow up: can videos ever have an alpha channel?
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
# convert frame to comfyui's expected format (taken from comfy's load image code)
|
||||
image = Image.fromarray(frame)
|
||||
image = ImageOps.exif_transpose(image)
|
||||
image = np.array(image, dtype=np.float32) / 255.0
|
||||
image = torch.from_numpy(image)[None,]
|
||||
images.append(image)
|
||||
frames_added += 1
|
||||
# if cap exists and we've reached it, stop processing frames
|
||||
if frame_load_cap > 0 and frames_added >= frame_load_cap:
|
||||
break
|
||||
finally:
|
||||
video_cap.release()
|
||||
|
||||
images = torch.cat(images, dim=0)
|
||||
|
||||
return (images, frames_added,)
|
||||
|
||||
def run(self, scenes_video,index):
|
||||
print('#scenes_video',index,scenes_video)
|
||||
index=index[0]
|
||||
if len(scenes_video) > index:
|
||||
vp=scenes_video[index]
|
||||
else:
|
||||
vp=scenes_video[-1]
|
||||
|
||||
return self.load_video_cv_fallback(vp,0,0)
|
||||
return (result, )
|
||||
@@ -0,0 +1,12 @@
|
||||
from VoiceStreamAI.asr.whisper_asr import WhisperASR
|
||||
from VoiceStreamAI.asr.faster_whisper_asr import FasterWhisperASR
|
||||
|
||||
class ASRFactory:
|
||||
@staticmethod
|
||||
def create_asr_pipeline(type, **kwargs):
|
||||
if type == "whisper":
|
||||
return WhisperASR(**kwargs)
|
||||
if type == "faster_whisper":
|
||||
return FasterWhisperASR(**kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unknown ASR pipeline type: {type}")
|
||||
@@ -0,0 +1,9 @@
|
||||
class ASRInterface:
|
||||
async def transcribe(self, client):
|
||||
"""
|
||||
Transcribe the given audio data.
|
||||
|
||||
:param client: The client object with all the member variables including the buffer
|
||||
:return: The transcription structure, see for example the faster_whisper_asr.py file.
|
||||
"""
|
||||
raise NotImplementedError("This method should be implemented by subclasses.")
|
||||
@@ -0,0 +1,142 @@
|
||||
import os
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
from VoiceStreamAI.asr.asr_interface import ASRInterface
|
||||
from VoiceStreamAI.audio_utils import save_audio_to_file
|
||||
|
||||
import folder_paths
|
||||
|
||||
language_codes = {
|
||||
"afrikaans": "af",
|
||||
"amharic": "am",
|
||||
"arabic": "ar",
|
||||
"assamese": "as",
|
||||
"azerbaijani": "az",
|
||||
"bashkir": "ba",
|
||||
"belarusian": "be",
|
||||
"bulgarian": "bg",
|
||||
"bengali": "bn",
|
||||
"tibetan": "bo",
|
||||
"breton": "br",
|
||||
"bosnian": "bs",
|
||||
"catalan": "ca",
|
||||
"czech": "cs",
|
||||
"welsh": "cy",
|
||||
"danish": "da",
|
||||
"german": "de",
|
||||
"greek": "el",
|
||||
"english": "en",
|
||||
"spanish": "es",
|
||||
"estonian": "et",
|
||||
"basque": "eu",
|
||||
"persian": "fa",
|
||||
"finnish": "fi",
|
||||
"faroese": "fo",
|
||||
"french": "fr",
|
||||
"galician": "gl",
|
||||
"gujarati": "gu",
|
||||
"hausa": "ha",
|
||||
"hawaiian": "haw",
|
||||
"hebrew": "he",
|
||||
"hindi": "hi",
|
||||
"croatian": "hr",
|
||||
"haitian": "ht",
|
||||
"hungarian": "hu",
|
||||
"armenian": "hy",
|
||||
"indonesian": "id",
|
||||
"icelandic": "is",
|
||||
"italian": "it",
|
||||
"japanese": "ja",
|
||||
"javanese": "jw",
|
||||
"georgian": "ka",
|
||||
"kazakh": "kk",
|
||||
"khmer": "km",
|
||||
"kannada": "kn",
|
||||
"korean": "ko",
|
||||
"latin": "la",
|
||||
"luxembourgish": "lb",
|
||||
"lingala": "ln",
|
||||
"lao": "lo",
|
||||
"lithuanian": "lt",
|
||||
"latvian": "lv",
|
||||
"malagasy": "mg",
|
||||
"maori": "mi",
|
||||
"macedonian": "mk",
|
||||
"malayalam": "ml",
|
||||
"mongolian": "mn",
|
||||
"marathi": "mr",
|
||||
"malay": "ms",
|
||||
"maltese": "mt",
|
||||
"burmese": "my",
|
||||
"nepali": "ne",
|
||||
"dutch": "nl",
|
||||
"norwegian nynorsk": "nn",
|
||||
"norwegian": "no",
|
||||
"occitan": "oc",
|
||||
"punjabi": "pa",
|
||||
"polish": "pl",
|
||||
"pashto": "ps",
|
||||
"portuguese": "pt",
|
||||
"romanian": "ro",
|
||||
"russian": "ru",
|
||||
"sanskrit": "sa",
|
||||
"sindhi": "sd",
|
||||
"sinhalese": "si",
|
||||
"slovak": "sk",
|
||||
"slovenian": "sl",
|
||||
"shona": "sn",
|
||||
"somali": "so",
|
||||
"albanian": "sq",
|
||||
"serbian": "sr",
|
||||
"sundanese": "su",
|
||||
"swedish": "sv",
|
||||
"swahili": "sw",
|
||||
"tamil": "ta",
|
||||
"telugu": "te",
|
||||
"tajik": "tg",
|
||||
"thai": "th",
|
||||
"turkmen": "tk",
|
||||
"tagalog": "tl",
|
||||
"turkish": "tr",
|
||||
"tatar": "tt",
|
||||
"ukrainian": "uk",
|
||||
"urdu": "ur",
|
||||
"uzbek": "uz",
|
||||
"vietnamese": "vi",
|
||||
"yiddish": "yi",
|
||||
"yoruba": "yo",
|
||||
"chinese": "zh",
|
||||
"cantonese": "yue",
|
||||
}
|
||||
|
||||
|
||||
class FasterWhisperASR(ASRInterface):
|
||||
def __init__(self, **kwargs):
|
||||
model_size = kwargs.get('model_size', "large-v3")
|
||||
device = kwargs.get('device', "cuda")
|
||||
model_root = os.path.join(folder_paths.models_dir, "whisper")
|
||||
# Run on GPU with FP16
|
||||
self.asr_pipeline = WhisperModel(model_size, device=device, compute_type="float16",download_root=model_root)
|
||||
|
||||
async def transcribe(self, client):
|
||||
file_path = await save_audio_to_file(client.scratch_buffer, client.get_file_name())
|
||||
|
||||
language = None if client.config['language'] is None else language_codes.get(client.config['language'].lower())
|
||||
segments, info = self.asr_pipeline.transcribe(file_path, word_timestamps=True, language=language)
|
||||
|
||||
segments = list(segments) # The transcription will actually run here.
|
||||
os.remove(file_path)
|
||||
|
||||
flattened_words = [word for segment in segments for word in segment.words]
|
||||
|
||||
to_return = {
|
||||
"language": info.language,
|
||||
"language_probability": info.language_probability,
|
||||
"text": ' '.join([s.text.strip() for s in segments]),
|
||||
"words":
|
||||
[
|
||||
{"word": w.word, "start": w.start, "end": w.end, "probability":w.probability} for w in flattened_words
|
||||
]
|
||||
}
|
||||
return to_return
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from transformers import pipeline
|
||||
from VoiceStreamAI.asr.asr_interface import ASRInterface
|
||||
from VoiceStreamAI.audio_utils import save_audio_to_file
|
||||
import os
|
||||
|
||||
class WhisperASR(ASRInterface):
|
||||
def __init__(self, **kwargs):
|
||||
model_name = kwargs.get('model_name', "openai/whisper-large-v3")
|
||||
self.asr_pipeline = pipeline("automatic-speech-recognition", model=model_name)
|
||||
|
||||
async def transcribe(self, client):
|
||||
file_path = await save_audio_to_file(client.scratch_buffer, client.get_file_name())
|
||||
|
||||
if client.config['language'] is not None:
|
||||
to_return = self.asr_pipeline(file_path, generate_kwargs={"language": client.config['language']})['text']
|
||||
else:
|
||||
to_return = self.asr_pipeline(file_path)['text']
|
||||
|
||||
os.remove(file_path)
|
||||
|
||||
to_return = {
|
||||
"language": "UNSUPPORTED_BY_HUGGINGFACE_WHISPER",
|
||||
"language_probability": None,
|
||||
"text": to_return.strip(),
|
||||
"words": "UNSUPPORTED_BY_HUGGINGFACE_WHISPER"
|
||||
}
|
||||
return to_return
|
||||
@@ -0,0 +1,26 @@
|
||||
import wave
|
||||
import os
|
||||
|
||||
async def save_audio_to_file(audio_data, file_name, audio_dir="audio_files", audio_format="wav"):
|
||||
"""
|
||||
Saves the audio data to a file.
|
||||
|
||||
:param client_id: Unique identifier for the client.
|
||||
:param audio_data: The audio data to save.
|
||||
:param file_counters: Dictionary to keep track of file counts for each client.
|
||||
:param audio_dir: Directory where audio files will be saved.
|
||||
:param audio_format: Format of the audio file.
|
||||
:return: Path to the saved audio file.
|
||||
"""
|
||||
|
||||
os.makedirs(audio_dir, exist_ok=True)
|
||||
|
||||
file_path = os.path.join(audio_dir, file_name)
|
||||
|
||||
with wave.open(file_path, 'wb') as wav_file:
|
||||
wav_file.setnchannels(1) # Assuming mono audio
|
||||
wav_file.setsampwidth(2)
|
||||
wav_file.setframerate(16000)
|
||||
wav_file.writeframes(audio_data)
|
||||
|
||||
return file_path
|
||||
@@ -0,0 +1,142 @@
|
||||
import os
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
|
||||
from VoiceStreamAI.buffering_strategy.buffering_strategy_interface import BufferingStrategyInterface
|
||||
from openai import OpenAI
|
||||
|
||||
class SilenceAtEndOfChunk(BufferingStrategyInterface):
|
||||
"""
|
||||
A buffering strategy that processes audio at the end of each chunk with silence detection.
|
||||
|
||||
This class is responsible for handling audio chunks, detecting silence at the end of each chunk,
|
||||
and initiating the transcription process for the chunk.
|
||||
|
||||
Attributes:
|
||||
client (Client): The client instance associated with this buffering strategy.
|
||||
chunk_length_seconds (float): Length of each audio chunk in seconds.
|
||||
chunk_offset_seconds (float): Offset time in seconds to be considered for processing audio chunks.
|
||||
"""
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
"""
|
||||
Initialize the SilenceAtEndOfChunk buffering strategy.
|
||||
|
||||
Args:
|
||||
client (Client): The client instance associated with this buffering strategy.
|
||||
**kwargs: Additional keyword arguments, including 'chunk_length_seconds' and 'chunk_offset_seconds'.
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
self.chunk_length_seconds = os.environ.get('BUFFERING_CHUNK_LENGTH_SECONDS')
|
||||
if not self.chunk_length_seconds:
|
||||
self.chunk_length_seconds = kwargs.get('chunk_length_seconds')
|
||||
self.chunk_length_seconds = float(self.chunk_length_seconds)
|
||||
|
||||
self.chunk_offset_seconds = os.environ.get('BUFFERING_CHUNK_OFFSET_SECONDS')
|
||||
if not self.chunk_offset_seconds:
|
||||
self.chunk_offset_seconds = kwargs.get('chunk_offset_seconds')
|
||||
self.chunk_offset_seconds = float(self.chunk_offset_seconds)
|
||||
|
||||
self.error_if_not_realtime = os.environ.get('ERROR_IF_NOT_REALTIME')
|
||||
if not self.error_if_not_realtime:
|
||||
self.error_if_not_realtime = kwargs.get('error_if_not_realtime', False)
|
||||
|
||||
self.processing_flag = False
|
||||
|
||||
self.messages=[]
|
||||
|
||||
def process_audio(self, websocket, vad_pipeline, asr_pipeline,llm_port):
|
||||
"""
|
||||
Process audio chunks by checking their length and scheduling asynchronous processing.
|
||||
|
||||
This method checks if the length of the audio buffer exceeds the chunk length and, if so,
|
||||
it schedules asynchronous processing of the audio.
|
||||
|
||||
Args:
|
||||
websocket (Websocket): The WebSocket connection for sending transcriptions.
|
||||
vad_pipeline: The voice activity detection pipeline.
|
||||
asr_pipeline: The automatic speech recognition pipeline.
|
||||
"""
|
||||
chunk_length_in_bytes = self.chunk_length_seconds * self.client.sampling_rate * self.client.samples_width
|
||||
if len(self.client.buffer) > chunk_length_in_bytes:
|
||||
if self.processing_flag:
|
||||
exit("Error in realtime processing: tried processing a new chunk while the previous one was still being processed")
|
||||
|
||||
self.client.scratch_buffer += self.client.buffer
|
||||
self.client.buffer.clear()
|
||||
self.processing_flag = True
|
||||
# Schedule the processing in a separate task
|
||||
asyncio.create_task(self.process_audio_async(websocket, vad_pipeline, asr_pipeline,llm_port))
|
||||
|
||||
async def process_audio_async(self, websocket, vad_pipeline, asr_pipeline,llm_port):
|
||||
"""
|
||||
Asynchronously process audio for activity detection and transcription.
|
||||
|
||||
This method performs heavy processing, including voice activity detection and transcription of
|
||||
the audio data. It sends the transcription results through the WebSocket connection.
|
||||
|
||||
Args:
|
||||
websocket (Websocket): The WebSocket connection for sending transcriptions.
|
||||
vad_pipeline: The voice activity detection pipeline.
|
||||
asr_pipeline: The automatic speech recognition pipeline.
|
||||
"""
|
||||
start = time.time()
|
||||
vad_results = await vad_pipeline.detect_activity(self.client)
|
||||
|
||||
if len(vad_results) == 0:
|
||||
self.client.scratch_buffer.clear()
|
||||
self.client.buffer.clear()
|
||||
self.processing_flag = False
|
||||
return
|
||||
|
||||
last_segment_should_end_before = ((len(self.client.scratch_buffer) / (self.client.sampling_rate * self.client.samples_width)) - self.chunk_offset_seconds)
|
||||
if vad_results[-1]['end'] < last_segment_should_end_before:
|
||||
transcription = await asr_pipeline.transcribe(self.client)
|
||||
if transcription['text'] != '':
|
||||
end = time.time()
|
||||
transcription['processing_time'] = end - start
|
||||
|
||||
transcription['status']="chat_start"
|
||||
|
||||
json_transcription = json.dumps(transcription)
|
||||
|
||||
await websocket.send(json_transcription)
|
||||
|
||||
# Point to the local server
|
||||
client = OpenAI(base_url=f"http://localhost:{llm_port}/v1", api_key="lm-studio")
|
||||
|
||||
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a friendly and engaging AI designed to interact with users in a conversational manner. Your personality is that of a sophisticated and polite young professional who is both a designer and a programmer. You are well-mannered, articulate, and possess a good sense of humor. Your goal is to provide helpful and insightful responses while maintaining a pleasant and enjoyable conversation. Be sure to use your knowledge in design and programming to enrich the dialogue and offer relevant advice or information when appropriate. Always be respectful and considerate of the user's feelings and perspectives. Additionally, you are fluent in both English and Chinese, and can seamlessly switch between the two languages to best assist users."},
|
||||
]+self.messages[-10:0]+[{"role": "user", "content":transcription['text']}]
|
||||
# print('#messages',messages)
|
||||
|
||||
completion = client.chat.completions.create(
|
||||
model="model-identifier",
|
||||
messages=messages,
|
||||
temperature=0.7,
|
||||
)
|
||||
|
||||
transcription['asistant'] = completion.choices[0].message.content
|
||||
|
||||
transcription['status']="chat_end"
|
||||
|
||||
json_transcription = json.dumps(transcription)
|
||||
|
||||
self.messages.append({
|
||||
"role": "user",
|
||||
"content":transcription['text']})
|
||||
|
||||
self.messages.append({
|
||||
"role": "asistant",
|
||||
"content": transcription['asistant']
|
||||
})
|
||||
# print('#messages',completion.choices[0].message.content)
|
||||
|
||||
await websocket.send(json_transcription)
|
||||
self.client.scratch_buffer.clear()
|
||||
self.client.increment_file_counter()
|
||||
|
||||
self.processing_flag = False
|
||||
@@ -0,0 +1,41 @@
|
||||
from VoiceStreamAI.buffering_strategy.buffering_strategies import SilenceAtEndOfChunk
|
||||
|
||||
class BufferingStrategyFactory:
|
||||
"""
|
||||
A factory class for creating instances of different buffering strategies.
|
||||
|
||||
This factory provides a centralized way to instantiate various buffering strategies
|
||||
based on the type specified. It abstracts the creation logic, making it easier to
|
||||
manage and extend with new buffering strategy types.
|
||||
|
||||
Methods:
|
||||
create_buffering_strategy: Creates and returns an instance of a specified buffering strategy.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_buffering_strategy(type, client, **kwargs):
|
||||
"""
|
||||
Creates an instance of a buffering strategy based on the specified type.
|
||||
|
||||
This method acts as a factory for creating buffering strategy objects. It returns
|
||||
an instance of the strategy corresponding to the given type. If the type is not
|
||||
recognized, it raises a ValueError.
|
||||
|
||||
Args:
|
||||
type (str): The type of buffering strategy to create. Currently supports 'silence_at_end_of_chunk'.
|
||||
client (Client): The client instance to be associated with the buffering strategy.
|
||||
**kwargs: Additional keyword arguments specific to the buffering strategy being created.
|
||||
|
||||
Returns:
|
||||
An instance of the specified buffering strategy.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified type is not recognized or supported.
|
||||
|
||||
Example:
|
||||
strategy = BufferingStrategyFactory.create_buffering_strategy("silence_at_end_of_chunk", client)
|
||||
"""
|
||||
if type == "silence_at_end_of_chunk":
|
||||
return SilenceAtEndOfChunk(client, **kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unknown buffering strategy type: {type}")
|
||||
@@ -0,0 +1,31 @@
|
||||
class BufferingStrategyInterface:
|
||||
"""
|
||||
An interface class for buffering strategies in audio processing systems.
|
||||
|
||||
This class defines the structure for buffering strategies used in handling
|
||||
and processing audio data. It serves as a template for creating custom buffering
|
||||
strategies that fit specific requirements of an audio processing pipeline.
|
||||
|
||||
Subclasses should implement the methods defined in this interface to ensure
|
||||
consistency and compatibility with the system's audio processing framework.
|
||||
|
||||
Methods:
|
||||
process_audio: Process audio data. This method should be implemented by subclasses.
|
||||
"""
|
||||
|
||||
def process_audio(self, websocket, vad_pipeline, asr_pipeline):
|
||||
"""
|
||||
Process audio data using the given WebSocket connection, VAD pipeline, and ASR pipeline.
|
||||
|
||||
This method is intended to be overridden in subclasses to provide specific logic
|
||||
for handling and processing audio data in different buffering strategies.
|
||||
|
||||
Args:
|
||||
websocket (Websocket): The WebSocket connection for communication with clients.
|
||||
vad_pipeline: The Voice Activity Detection (VAD) pipeline used for detecting speech in the audio.
|
||||
asr_pipeline: The Automatic Speech Recognition (ASR) pipeline used for transcribing speech in the audio.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If the method is not implemented in the subclass.
|
||||
"""
|
||||
raise NotImplementedError("This method should be implemented by subclasses.")
|
||||
@@ -0,0 +1,54 @@
|
||||
from VoiceStreamAI.buffering_strategy.buffering_strategy_factory import BufferingStrategyFactory
|
||||
|
||||
class Client:
|
||||
"""
|
||||
Represents a client connected to the VoiceStreamAI server.
|
||||
|
||||
This class maintains the state for each connected client, including their
|
||||
unique identifier, audio buffer, configuration, and a counter for processed audio files.
|
||||
|
||||
Attributes:
|
||||
client_id (str): A unique identifier for the client.
|
||||
buffer (bytearray): A buffer to store incoming audio data.
|
||||
config (dict): Configuration settings for the client, like chunk length and offset.
|
||||
file_counter (int): Counter for the number of audio files processed.
|
||||
total_samples (int): Total number of audio samples received from this client.
|
||||
sampling_rate (int): The sampling rate of the audio data in Hz.
|
||||
samples_width (int): The width of each audio sample in bits.
|
||||
"""
|
||||
def __init__(self, client_id, sampling_rate, samples_width):
|
||||
self.client_id = client_id
|
||||
self.buffer = bytearray()
|
||||
self.scratch_buffer = bytearray()
|
||||
self.config = {"language": None,
|
||||
"processing_strategy": "silence_at_end_of_chunk",
|
||||
"processing_args": {
|
||||
"chunk_length_seconds": 5,
|
||||
"chunk_offset_seconds": 0.1
|
||||
}
|
||||
}
|
||||
self.file_counter = 0
|
||||
self.total_samples = 0
|
||||
self.sampling_rate = sampling_rate
|
||||
self.samples_width = samples_width
|
||||
self.buffering_strategy = BufferingStrategyFactory.create_buffering_strategy(self.config['processing_strategy'], self, **self.config['processing_args'])
|
||||
|
||||
def update_config(self, config_data):
|
||||
self.config.update(config_data)
|
||||
self.buffering_strategy = BufferingStrategyFactory.create_buffering_strategy(self.config['processing_strategy'], self, **self.config['processing_args'])
|
||||
|
||||
def append_audio_data(self, audio_data):
|
||||
self.buffer.extend(audio_data)
|
||||
self.total_samples += len(audio_data) / self.samples_width
|
||||
|
||||
def clear_buffer(self):
|
||||
self.buffer.clear()
|
||||
|
||||
def increment_file_counter(self):
|
||||
self.file_counter += 1
|
||||
|
||||
def get_file_name(self):
|
||||
return f"{self.client_id}_{self.file_counter}.wav"
|
||||
|
||||
def process_audio(self, websocket, vad_pipeline, asr_pipeline,llm_port):
|
||||
self.buffering_strategy.process_audio(websocket, vad_pipeline, asr_pipeline,llm_port)
|
||||
@@ -0,0 +1,54 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# 获取当前文件的绝对路径
|
||||
current_file_path = os.path.abspath(__file__)
|
||||
|
||||
# 获取当前文件的目录
|
||||
current_directory = os.path.dirname(current_file_path)
|
||||
sys.path.append(str(Path(current_directory).parent))
|
||||
# print("sys.path", current_directory)
|
||||
|
||||
|
||||
from VoiceStreamAI.server import Server
|
||||
from VoiceStreamAI.asr.asr_factory import ASRFactory
|
||||
from VoiceStreamAI.vad.vad_factory import VADFactory
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="VoiceStreamAI Server: Real-time audio transcription using self-hosted Whisper and WebSocket")
|
||||
parser.add_argument("--vad-type", type=str, default="pyannote", help="Type of VAD pipeline to use (e.g., 'pyannote')")
|
||||
parser.add_argument("--vad-args", type=str, default='{"auth_token": "huggingface_token"}', help="JSON string of additional arguments for VAD pipeline")
|
||||
parser.add_argument("--asr-type", type=str, default="faster_whisper", help="Type of ASR pipeline to use (e.g., 'whisper')")
|
||||
parser.add_argument("--asr-args", type=str, default='{"model_size": "large-v3"}', help="JSON string of additional arguments for ASR pipeline")
|
||||
parser.add_argument("--host", type=str, default="127.0.0.1", help="Host for the WebSocket server")
|
||||
parser.add_argument("--port", type=int, default=8765, help="Port for the WebSocket server")
|
||||
parser.add_argument("--certfile", type=str, default=None, help="The path to the SSL certificate (cert file) if using secure websockets")
|
||||
parser.add_argument("--keyfile", type=str, default=None, help="The path to the SSL key file if using secure websockets")
|
||||
return parser.parse_args()
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
try:
|
||||
vad_args = json.loads(args.vad_args)
|
||||
asr_args = json.loads(args.asr_args)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing JSON arguments: {e}")
|
||||
return
|
||||
|
||||
vad_pipeline = VADFactory.create_vad_pipeline(args.vad_type, **vad_args)
|
||||
asr_pipeline = ASRFactory.create_asr_pipeline(args.asr_type, **asr_args)
|
||||
|
||||
server = Server(vad_pipeline, asr_pipeline, host=args.host, port=args.port, sampling_rate=16000, samples_width=2, certfile=args.certfile, keyfile=args.keyfile)
|
||||
|
||||
asyncio.get_event_loop().run_until_complete(server.start())
|
||||
asyncio.get_event_loop().run_forever()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
websockets
|
||||
speechbrain
|
||||
pyannote-audio
|
||||
asyncio
|
||||
sentence-transformers
|
||||
transformers
|
||||
faster-whisper
|
||||
@@ -0,0 +1,88 @@
|
||||
import websockets
|
||||
import uuid
|
||||
import json
|
||||
import asyncio
|
||||
import ssl
|
||||
|
||||
from VoiceStreamAI.audio_utils import save_audio_to_file
|
||||
from VoiceStreamAI.client import Client
|
||||
|
||||
class Server:
|
||||
"""
|
||||
Represents the WebSocket server for handling real-time audio transcription.
|
||||
|
||||
This class manages WebSocket connections, processes incoming audio data,
|
||||
and interacts with VAD and ASR pipelines for voice activity detection and
|
||||
speech recognition.
|
||||
|
||||
Attributes:
|
||||
vad_pipeline: An instance of a voice activity detection pipeline.
|
||||
asr_pipeline: An instance of an automatic speech recognition pipeline.
|
||||
host (str): Host address of the server.
|
||||
port (int): Port on which the server listens.
|
||||
sampling_rate (int): The sampling rate of audio data in Hz.
|
||||
samples_width (int): The width of each audio sample in bits.
|
||||
connected_clients (dict): A dictionary mapping client IDs to Client objects.
|
||||
"""
|
||||
def __init__(self, vad_pipeline, asr_pipeline, host='localhost', port=8765, sampling_rate=16000, samples_width=2, certfile = None, keyfile = None,llm_port=9000):
|
||||
self.vad_pipeline = vad_pipeline
|
||||
self.asr_pipeline = asr_pipeline
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.sampling_rate = sampling_rate
|
||||
self.samples_width = samples_width
|
||||
self.certfile = certfile
|
||||
self.keyfile = keyfile
|
||||
self.connected_clients = {}
|
||||
|
||||
self.llm_port=llm_port
|
||||
|
||||
async def handle_audio(self, client, websocket):
|
||||
while True:
|
||||
message = await websocket.recv()
|
||||
|
||||
if isinstance(message, bytes):
|
||||
client.append_audio_data(message)
|
||||
elif isinstance(message, str):
|
||||
config = json.loads(message)
|
||||
if config.get('type') == 'config':
|
||||
client.update_config(config['data'])
|
||||
continue
|
||||
else:
|
||||
print(f"Unexpected message type from {client.client_id}")
|
||||
|
||||
# this is synchronous, any async operation is in BufferingStrategy
|
||||
client.process_audio(websocket, self.vad_pipeline, self.asr_pipeline,self.llm_port)
|
||||
|
||||
|
||||
async def handle_websocket(self, websocket, path):
|
||||
client_id = str(uuid.uuid4())
|
||||
client = Client(client_id, self.sampling_rate, self.samples_width)
|
||||
self.connected_clients[client_id] = client
|
||||
|
||||
print(f"Client {client_id} connected")
|
||||
|
||||
try:
|
||||
await self.handle_audio(client, websocket)
|
||||
except websockets.ConnectionClosed as e:
|
||||
print(f"Connection with {client_id} closed: {e}")
|
||||
finally:
|
||||
del self.connected_clients[client_id]
|
||||
|
||||
def start(self):
|
||||
if self.certfile:
|
||||
# Create an SSL context to enforce encrypted connections
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
|
||||
# Load your server's certificate and private key
|
||||
# Replace 'your_cert_path.pem' and 'your_key_path.pem' with the actual paths to your files
|
||||
ssl_context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
|
||||
|
||||
print(f"WebSocket server ready to accept secure connections on {self.host}:{self.port}")
|
||||
|
||||
# Pass the SSL context to the serve function along with the host and port
|
||||
# Ensure the secure flag is set to True if using a secure WebSocket protocol (wss://)
|
||||
return websockets.serve(self.handle_websocket, self.host, self.port, ssl=ssl_context)
|
||||
else:
|
||||
print(f"WebSocket server ready to accept secure connections on {self.host}:{self.port}")
|
||||
return websockets.serve(self.handle_websocket, self.host, self.port)
|
||||
@@ -0,0 +1,50 @@
|
||||
from os import remove
|
||||
import os
|
||||
|
||||
from pyannote.core import Segment
|
||||
from pyannote.audio import Model
|
||||
from pyannote.audio.pipelines import VoiceActivityDetection
|
||||
|
||||
from VoiceStreamAI.vad.vad_interface import VADInterface
|
||||
from VoiceStreamAI.audio_utils import save_audio_to_file
|
||||
|
||||
|
||||
class PyannoteVAD(VADInterface):
|
||||
"""
|
||||
Pyannote-based implementation of the VADInterface.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Initializes Pyannote's VAD pipeline.
|
||||
|
||||
Args:
|
||||
model_name (str): The model name for Pyannote.
|
||||
auth_token (str, optional): Authentication token for Hugging Face.
|
||||
"""
|
||||
|
||||
model_name = kwargs.get('model_name', "pyannote/segmentation")
|
||||
|
||||
auth_token = os.environ.get('PYANNOTE_AUTH_TOKEN')
|
||||
if not auth_token:
|
||||
auth_token = kwargs.get('auth_token')
|
||||
|
||||
if auth_token is None:
|
||||
raise ValueError("Missing required env var in PYANNOTE_AUTH_TOKEN or argument in --vad-args: 'auth_token'")
|
||||
|
||||
pyannote_args = kwargs.get('pyannote_args', {"onset": 0.5, "offset": 0.5, "min_duration_on": 0.3, "min_duration_off": 0.3})
|
||||
self.model = Model.from_pretrained(model_name, use_auth_token=auth_token)
|
||||
self.vad_pipeline = VoiceActivityDetection(segmentation=self.model)
|
||||
self.vad_pipeline.instantiate(pyannote_args)
|
||||
|
||||
async def detect_activity(self, client):
|
||||
audio_file_path = await save_audio_to_file(client.scratch_buffer, client.get_file_name())
|
||||
vad_results = self.vad_pipeline(audio_file_path)
|
||||
remove(audio_file_path)
|
||||
vad_segments = []
|
||||
if len(vad_results) > 0:
|
||||
vad_segments = [
|
||||
{"start": segment.start, "end": segment.end, "confidence": 1.0}
|
||||
for segment in vad_results.itersegments()
|
||||
]
|
||||
return vad_segments
|
||||
@@ -0,0 +1,23 @@
|
||||
from VoiceStreamAI.vad.pyannote_vad import PyannoteVAD
|
||||
|
||||
class VADFactory:
|
||||
"""
|
||||
Factory for creating instances of VAD systems.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_vad_pipeline(type, **kwargs):
|
||||
"""
|
||||
Creates a VAD pipeline based on the specified type.
|
||||
|
||||
Args:
|
||||
type (str): The type of VAD pipeline to create (e.g., 'pyannote').
|
||||
kwargs: Additional arguments for the VAD pipeline creation.
|
||||
|
||||
Returns:
|
||||
VADInterface: An instance of a class that implements VADInterface.
|
||||
"""
|
||||
if type == "pyannote":
|
||||
return PyannoteVAD(**kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unknown VAD pipeline type: {type}")
|
||||
@@ -0,0 +1,16 @@
|
||||
class VADInterface:
|
||||
"""
|
||||
Interface for voice activity detection (VAD) systems.
|
||||
"""
|
||||
|
||||
async def detect_activity(self, client):
|
||||
"""
|
||||
Detects voice activity in the given audio data.
|
||||
|
||||
Args:
|
||||
client (src.Client): The client to detect on
|
||||
|
||||
Returns:
|
||||
List: VAD result, a list of objects containing "start", "end", "confidence"
|
||||
"""
|
||||
raise NotImplementedError("This method should be implemented by subclasses.")
|
||||
@@ -1,172 +0,0 @@
|
||||
import torch
|
||||
from PIL import Image, ImageOps, ImageSequence, ImageFile
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
|
||||
import numpy as np
|
||||
import os
|
||||
import folder_paths
|
||||
import node_helpers
|
||||
import hashlib
|
||||
|
||||
# Tensor to PIL
|
||||
def tensor2pil(image):
|
||||
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
|
||||
# tensor 取hash值
|
||||
def tensor_to_hash(tensor):
|
||||
# 将 Tensor 转换为 NumPy 数组
|
||||
np_array = tensor.cpu().numpy()
|
||||
|
||||
# 将 NumPy 数组转换为字节数据
|
||||
byte_data = np_array.tobytes()
|
||||
|
||||
# 计算哈希值
|
||||
hash_value = hashlib.md5(byte_data).hexdigest()
|
||||
|
||||
return hash_value
|
||||
|
||||
|
||||
def create_temp_file(image):
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
|
||||
(
|
||||
full_output_folder,
|
||||
filename,
|
||||
counter,
|
||||
subfolder,
|
||||
_,
|
||||
) = folder_paths.get_save_image_path('material', output_dir)
|
||||
|
||||
|
||||
image=tensor2pil(image)
|
||||
|
||||
image_file = f"{filename}_{counter:05}.png"
|
||||
|
||||
image_path=os.path.join(full_output_folder, image_file)
|
||||
|
||||
image.save(image_path,compress_level=4)
|
||||
|
||||
return (image_path,[{
|
||||
"filename": image_file,
|
||||
"subfolder": subfolder,
|
||||
"type": "temp"
|
||||
}])
|
||||
|
||||
|
||||
# image - tensor - 文件路径
|
||||
# loadImage的方法( 文件路径 - image-mask )
|
||||
class EditMask:
|
||||
|
||||
def __init__(self):
|
||||
self.image_id = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required":
|
||||
{"image": ("IMAGE",), # 表示一个张量
|
||||
|
||||
},
|
||||
|
||||
"optional":{
|
||||
"image_update": ("IMAGE_FILE",)
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
CATEGORY = "♾️Mixlab/Mask"
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask")
|
||||
|
||||
FUNCTION = "edit"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def edit(self, image,image_update=None):
|
||||
|
||||
# 根据image输入来判断是否是新的图片
|
||||
if self.image_id==None:
|
||||
self.image_id=tensor_to_hash(image)
|
||||
image_update=None
|
||||
else:
|
||||
image_id=tensor_to_hash(image)
|
||||
if image_id!=self.image_id:
|
||||
image_update=None
|
||||
self.image_id=image_id
|
||||
|
||||
|
||||
image_path=None
|
||||
# print('#image_update',self.image_id,image_update)
|
||||
if image_update==None:
|
||||
print('--')
|
||||
else:
|
||||
if 'images' in image_update:
|
||||
images=image_update['images']
|
||||
filename=images[0]['filename']
|
||||
subfolder=images[0]['subfolder']
|
||||
type=images[0]['type']
|
||||
name, base_dir=folder_paths.annotated_filepath(filename)
|
||||
if type.endswith("output"):
|
||||
base_dir = folder_paths.get_output_directory()
|
||||
elif type.endswith("input"):
|
||||
base_dir = folder_paths.get_input_directory()
|
||||
elif type.endswith("temp"):
|
||||
base_dir = folder_paths.get_temp_directory()
|
||||
#base_dir = folder_paths.get_input_directory()
|
||||
# print(base_dir,subfolder, name)
|
||||
image_path = os.path.join(base_dir,subfolder, name)
|
||||
|
||||
if image_path==None:
|
||||
image_path,images=create_temp_file(image)
|
||||
|
||||
print('#image_path',os.path.exists(image_path),image_path)
|
||||
# image_path = folder_paths.get_annotated_filepath(image) #文件名
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
image_path,images=create_temp_file(image)
|
||||
|
||||
|
||||
img = node_helpers.pillow(Image.open, image_path)
|
||||
|
||||
output_images = []
|
||||
output_masks = []
|
||||
w, h = None, None
|
||||
|
||||
excluded_formats = ['MPO']
|
||||
|
||||
for i in ImageSequence.Iterator(img):
|
||||
i = node_helpers.pillow(ImageOps.exif_transpose, i)
|
||||
|
||||
if i.mode == 'I':
|
||||
i = i.point(lambda i: i * (1 / 255))
|
||||
image = i.convert("RGB")
|
||||
|
||||
if len(output_images) == 0:
|
||||
w = image.size[0]
|
||||
h = image.size[1]
|
||||
|
||||
if image.size[0] != w or image.size[1] != h:
|
||||
continue
|
||||
|
||||
image = np.array(image).astype(np.float32) / 255.0
|
||||
image = torch.from_numpy(image)[None,]
|
||||
if 'A' in i.getbands():
|
||||
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
|
||||
mask = 1. - torch.from_numpy(mask)
|
||||
else:
|
||||
# 尺寸不对,需要按照image来
|
||||
mask = torch.zeros((h, w), dtype=torch.float32, device="cpu")
|
||||
|
||||
output_images.append(image)
|
||||
output_masks.append(mask.unsqueeze(0))
|
||||
|
||||
if len(output_images) > 1 and img.format not in excluded_formats:
|
||||
output_image = torch.cat(output_images, dim=0)
|
||||
output_mask = torch.cat(output_masks, dim=0)
|
||||
else:
|
||||
output_image = output_images[0]
|
||||
output_mask = output_masks[0]
|
||||
|
||||
return {"ui":{"images": images},"result": (output_image, output_mask)}
|
||||
|
||||
# return (output_image, output_mask)
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-mixlab-nodes"
|
||||
description = "3D, ScreenShareNode & FloatingVideoNode, SpeechRecognition & SpeechSynthesis, GPT, LoadImagesFromLocal, Layers, Other Nodes, ..."
|
||||
version = "0.30.3"
|
||||
version = "0.28.3"
|
||||
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"]
|
||||
|
||||
|
||||
+1
-3
@@ -15,6 +15,4 @@ Pillow>=9.5.0
|
||||
einops==0.7.0
|
||||
trimesh>=4.0.5
|
||||
huggingface-hub
|
||||
scikit-image
|
||||
torchaudio
|
||||
soundfile>=0.12.1
|
||||
scikit-image
|
||||
+2
-84
@@ -1388,7 +1388,7 @@
|
||||
}
|
||||
|
||||
// video ,gif
|
||||
if (["VHS_VideoCombine", "VideoCombine_Adv", "CombineAudioVideo"].includes(node.class_type)) {
|
||||
if (["VHS_VideoCombine", "VideoCombine_Adv"].includes(node.class_type)) {
|
||||
|
||||
let a = document.createElement('a');
|
||||
a.id = `output_${node.id}`
|
||||
@@ -1645,42 +1645,6 @@
|
||||
// copyImagesToClipboard(htmlString);
|
||||
|
||||
|
||||
// 上传音频转为base64
|
||||
async function uploadAndConvertAudio(file) {
|
||||
if (!file) {
|
||||
alert('Please select a WAV file.')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.type !== 'audio/wav') {
|
||||
alert('Only WAV files are supported.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const base64Audio = await readFileAsDataURL(file)
|
||||
return base64Audio
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error)
|
||||
alert('Error reading file.')
|
||||
}
|
||||
}
|
||||
|
||||
function readFileAsDataURL(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = function (event) {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
|
||||
reader.onerror = function (error) {
|
||||
reject(error)
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
function createInputs(inputData) {
|
||||
// Assuming you have an HTML element with the id "container" to hold the UI
|
||||
@@ -2127,53 +2091,7 @@
|
||||
container.appendChild(d);
|
||||
}
|
||||
|
||||
// 加载音频 base64
|
||||
if (['LoadAndCombinedAudio_'].includes(data.class_type)) {
|
||||
let inpAudio = document.createElement('div');
|
||||
|
||||
let inputAudio = document.createElement('button');
|
||||
|
||||
inputAudio.innerText = data.title || 'Upload Audio'
|
||||
inputAudio.style = `cursor: pointer;
|
||||
font-weight: 300;
|
||||
margin: 2px;
|
||||
color: var(--descrip-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;height: 30px;min-width: 122px;
|
||||
`;
|
||||
|
||||
let audioE = document.createElement('audio');
|
||||
audioE.setAttribute('controls', 'on')
|
||||
//暂时只支持一个
|
||||
audioE.src = data.inputs.audios.base64[0];
|
||||
|
||||
inputAudio.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
let inp = document.createElement('input')
|
||||
inp.type = 'file'
|
||||
inp.style.display = 'none'
|
||||
inp.addEventListener('change', async e => {
|
||||
e.preventDefault()
|
||||
const file = e.target.files[0]
|
||||
let base64 = await uploadAndConvertAudio(file)
|
||||
|
||||
audioE.src = base64;
|
||||
|
||||
window._appData.data[data.id].inputs.audios.base64 = [base64];
|
||||
})
|
||||
|
||||
inp.click()
|
||||
inp.remove()
|
||||
})
|
||||
|
||||
inpAudio.appendChild(inputAudio)
|
||||
inpAudio.appendChild(audioE)
|
||||
|
||||
container.appendChild(inpAudio);
|
||||
|
||||
}
|
||||
});
|
||||
return container
|
||||
}
|
||||
@@ -3460,7 +3378,7 @@
|
||||
|
||||
createAppList(window._apps);
|
||||
|
||||
// if (window._apps.length > 0) await createHistoryList(category || '');
|
||||
if (window._apps.length > 0) await createHistoryList(category || '');
|
||||
|
||||
createApp(window._appData);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { app } from '../../../scripts/app.js'
|
||||
import { $el } from '../../../scripts/ui.js'
|
||||
import { api } from '../../../scripts/api.js'
|
||||
|
||||
import { td_bg } from './td_background.js'
|
||||
console.log('td_bg', td_bg)
|
||||
//本机安装的插件节点全集
|
||||
window._nodesAll = null
|
||||
|
||||
@@ -187,25 +185,6 @@ async function extractInputAndOutputData (
|
||||
if (node.type == 'Color') {
|
||||
}
|
||||
|
||||
// 语音输入的支持
|
||||
if (node.type == 'LoadAndCombinedAudio_') {
|
||||
// if (
|
||||
// data[id].widgets_values &&
|
||||
// data[id].widgets_values[0] &&
|
||||
// data[id].widgets_values[0].base64 &&
|
||||
// data[id].widgets_values[0].base64.length > 0
|
||||
// ) {
|
||||
// options.defaultBase64 = data[id].widgets_values[0].base64
|
||||
// }
|
||||
|
||||
input[inputIds.indexOf(id)] = {
|
||||
...data[id],
|
||||
title: node.title,
|
||||
id,
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'LoadImage') {
|
||||
// loadImage的mask支持
|
||||
let output = node.outputs.filter(ot => ot.type == 'MASK')[0]
|
||||
@@ -418,11 +397,11 @@ async function save (json, download = false, showInfo = true) {
|
||||
|
||||
function getInputsAndOutputs () {
|
||||
const inputs =
|
||||
`LoadImage LoadImagesToBatch ImagesPrompt_ LoadAndCombinedAudio_ LoadVideoAndSegment_ VHS_LoadVideo CLIPTextEncode PromptSlide TextInput_ Color FloatSlider IntNumber CheckpointLoaderSimple LoraLoader`.split(
|
||||
`LoadImage LoadImagesToBatch ImagesPrompt_ VHS_LoadVideo CLIPTextEncode PromptSlide TextInput_ Color FloatSlider IntNumber CheckpointLoaderSimple LoraLoader`.split(
|
||||
' '
|
||||
),
|
||||
outputs =
|
||||
`SaveTripoSRMesh,PreviewImage,SaveImage,TransparentImage,ShowTextForGPT,CombineAudioVideo,VHS_VideoCombine,VideoCombine_Adv,Image Save,SaveImageAndMetadata_,ClipInterrogator`.split(
|
||||
`SaveTripoSRMesh,PreviewImage,SaveImage,TransparentImage,ShowTextForGPT,VHS_VideoCombine,VideoCombine_Adv,Image Save,SaveImageAndMetadata_,ClipInterrogator`.split(
|
||||
','
|
||||
)
|
||||
|
||||
@@ -524,21 +503,6 @@ app.registerExtension({
|
||||
}
|
||||
})
|
||||
|
||||
//td bg
|
||||
const tdBG = document.createElement('button')
|
||||
tdBG.innerText = 'Canvas Mode'
|
||||
tdBG.style = style
|
||||
tdBG.style.marginLeft = '12px'
|
||||
|
||||
tdBG.addEventListener('click', () => {
|
||||
td_bg.toggle()
|
||||
if (td_bg.running) {
|
||||
tdBG.style.background = 'yellow'
|
||||
} else {
|
||||
tdBG.style.background = 'transparent'
|
||||
}
|
||||
})
|
||||
|
||||
// author
|
||||
let author = document.createElement('div')
|
||||
// author.style=`display: flex`
|
||||
@@ -695,7 +659,6 @@ app.registerExtension({
|
||||
|
||||
btns.appendChild(btn)
|
||||
btns.appendChild(download)
|
||||
btns.appendChild(tdBG)
|
||||
|
||||
document.body.appendChild(widget.div)
|
||||
this.addCustomWidget(widget)
|
||||
@@ -709,7 +672,6 @@ app.registerExtension({
|
||||
this.serialize_widgets = true //需要保存参数
|
||||
|
||||
window._mixlab_app_json = null
|
||||
|
||||
}
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
@@ -725,8 +687,9 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
const div = this.widgets.filter(w => w.div)[0].div
|
||||
Array.from(div.querySelectorAll('button'), b =>
|
||||
b.innerText != 'Canvas Mode' ? (b.style.background = 'yellow') : ''
|
||||
Array.from(
|
||||
div.querySelectorAll('button'),
|
||||
b => (b.style.background = 'yellow')
|
||||
)
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
@@ -396,217 +396,3 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 上传音频转为base64
|
||||
async function uploadAndConvertAudio (file) {
|
||||
if (!file) {
|
||||
alert('Please select a WAV file.')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.type !== 'audio/wav') {
|
||||
alert('Only WAV files are supported.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const base64Audio = await readFileAsDataURL(file)
|
||||
return base64Audio
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error)
|
||||
alert('Error reading file.')
|
||||
}
|
||||
}
|
||||
|
||||
function readFileAsDataURL (file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = function (event) {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
|
||||
reader.onerror = function (error) {
|
||||
reject(error)
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
const createInputAudioForBatch = (base64, widget) => {
|
||||
// Create an audio element
|
||||
let audio = document.createElement('audio')
|
||||
audio.src = base64
|
||||
audio.controls = true
|
||||
audio.style = 'width: 120px; display: block'
|
||||
|
||||
// Create a delete button
|
||||
let deleteButton = document.createElement('button')
|
||||
deleteButton.textContent = 'Delete'
|
||||
|
||||
deleteButton.style = `cursor: pointer;
|
||||
font-weight: 300;
|
||||
margin: 2px;
|
||||
margin-left: 10px;
|
||||
color: var(--descrip-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;height: 30px;min-width: 122px;
|
||||
`
|
||||
|
||||
// Create a container for the audio and delete button
|
||||
let container = document.createElement('div')
|
||||
container.appendChild(audio)
|
||||
container.appendChild(deleteButton)
|
||||
container.style = `display: flex;margin-top: 12px;`
|
||||
|
||||
// Add event listener for the delete button
|
||||
deleteButton.addEventListener('click', e => {
|
||||
let newValue = []
|
||||
let items = widget.value?.base64 || []
|
||||
for (const v of items) {
|
||||
if (v != base64) newValue.push(v)
|
||||
}
|
||||
widget.value.base64 = newValue
|
||||
container.remove()
|
||||
})
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Mixlab.Comfy.LoadAndCombinedAudio_',
|
||||
async getCustomWidgets (app) {
|
||||
return {
|
||||
AUDIOBASE64 (node, inputName, inputData, app) {
|
||||
// console.log('##node', node)
|
||||
const widget = {
|
||||
value: {
|
||||
base64: []
|
||||
}, // 不能[x,x,x]
|
||||
type: inputData[0], // the type
|
||||
name: inputName, // the name, slice
|
||||
size: [128, 32], // a default size
|
||||
draw (ctx, node, width, y) {},
|
||||
computeSize (...args) {
|
||||
return [128, 122] // a method to compute the current size of the widget
|
||||
}
|
||||
// serializeValue (nodeId, widgetIndex) {
|
||||
// return widget.value
|
||||
// },
|
||||
}
|
||||
// widget.something = something; // maybe adds stuff to it
|
||||
node.addCustomWidget(widget) // adds it to the node
|
||||
return widget // and returns it.
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async beforeRegisterNodeDef (nodeType, nodeData, app) {
|
||||
if (nodeType.comfyClass == 'LoadAndCombinedAudio_') {
|
||||
const orig_nodeCreated = nodeType.prototype.onNodeCreated
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
orig_nodeCreated?.apply(this, arguments)
|
||||
|
||||
let audiosWidget = this.widgets.filter(w => w.name == 'audios')[0]
|
||||
|
||||
const widget = {
|
||||
type: 'div',
|
||||
name: 'audio_base64',
|
||||
draw (ctx, node, widget_width, y, widget_height) {
|
||||
Object.assign(
|
||||
this.div.style,
|
||||
get_position_style(ctx, widget_width, 44, node.size[1])
|
||||
)
|
||||
},
|
||||
serialize: false
|
||||
}
|
||||
|
||||
widget.div = $el('div', {})
|
||||
|
||||
document.body.appendChild(widget.div)
|
||||
|
||||
let audioPreview = document.createElement('div')
|
||||
let audiosDiv = document.createElement('div') //显示图片
|
||||
audiosDiv.className = 'audios_preview'
|
||||
audiosDiv.style = `width: calc(100% - 14px);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 7px; justify-content: space-between;
|
||||
align-items: center;`
|
||||
|
||||
const btn = document.createElement('button')
|
||||
btn.innerText = 'Upload Audio'
|
||||
|
||||
btn.style = `cursor: pointer;
|
||||
font-weight: 300;
|
||||
margin: 2px;
|
||||
color: var(--descrip-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;height: 30px;min-width: 122px;
|
||||
`
|
||||
|
||||
btn.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
let inputAudio = document.createElement('input')
|
||||
inputAudio.type = 'file'
|
||||
inputAudio.style.display = 'none'
|
||||
inputAudio.addEventListener('change', async e => {
|
||||
e.preventDefault()
|
||||
const file = e.target.files[0]
|
||||
let base64 = await uploadAndConvertAudio(file)
|
||||
if (!audiosWidget.value) audiosWidget.value = { base64: [] }
|
||||
audiosWidget.value.base64.push(base64)
|
||||
|
||||
let a = createInputAudioForBatch(base64, audiosWidget)
|
||||
audiosDiv.appendChild(a)
|
||||
})
|
||||
|
||||
inputAudio.click()
|
||||
inputAudio.remove()
|
||||
})
|
||||
|
||||
widget.div.appendChild(audioPreview)
|
||||
audioPreview.appendChild(audiosDiv)
|
||||
audioPreview.appendChild(btn)
|
||||
// audioPreview.appendChild(inputAudio)
|
||||
|
||||
this.addCustomWidget(widget)
|
||||
|
||||
// document.addEventListener('wheel', handleMouseWheel)
|
||||
|
||||
const onRemoved = this.onRemoved
|
||||
this.onRemoved = () => {
|
||||
widget.div.remove()
|
||||
try {
|
||||
// document.removeEventListener('wheel', handleMouseWheel)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
return onRemoved?.()
|
||||
}
|
||||
|
||||
this.serialize_widgets = true //需要保存参数
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadedGraphNode (node, app) {
|
||||
if (node.type === 'LoadAndCombinedAudio_') {
|
||||
// await sleep(0)
|
||||
let audiosWidget = node.widgets.filter(w => w.name === 'audios')[0]
|
||||
let audioPreview = node.widgets.filter(w => w.name == 'audio_base64')[0]
|
||||
|
||||
let pre = audioPreview.div.querySelector('.audios_preview')
|
||||
for (const d of audiosWidget.value?.base64 || []) {
|
||||
let im = createInputAudioForBatch(d, audiosWidget)
|
||||
pre.appendChild(im)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
|
||||
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
|
||||
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
|
||||
|
||||
const version = 'v0.30.3'
|
||||
const version = 'v0.28.3'
|
||||
|
||||
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -209,16 +209,13 @@ app.registerExtension({
|
||||
text = text.filter(t => t && t?.trim())
|
||||
|
||||
if (this.widgets) {
|
||||
// console.log('#ShowTextForGPT',this.widgets)
|
||||
// const pos = this.widgets.findIndex(w => w.name === 'text')
|
||||
for (let i = 0; i < this.widgets.length; i++) {
|
||||
if (this.widgets[i].name == 'show_text')
|
||||
this.widgets[i].onRemove?.()
|
||||
console.log('#ShowTextForGPT', this.widgets[i])
|
||||
if (this.widgets[i].name == 'show_text') this.widgets[i].onRemove?.()
|
||||
}
|
||||
this.widgets.length = 2
|
||||
this.widgets.length = 1
|
||||
}
|
||||
|
||||
// console.log('ShowTextForGPT',text)
|
||||
for (let list of text) {
|
||||
if (list) {
|
||||
// console.log('#####', list)
|
||||
@@ -231,8 +228,6 @@ app.registerExtension({
|
||||
w.inputEl.readOnly = true
|
||||
w.inputEl.style.opacity = 0.6
|
||||
|
||||
// w.inputEl.style.display='none'
|
||||
|
||||
try {
|
||||
if (typeof list != 'string') {
|
||||
let data = JSON.parse(list)
|
||||
@@ -285,24 +280,5 @@ app.registerExtension({
|
||||
|
||||
this.serialize_widgets = true //需要保存参数
|
||||
}
|
||||
},
|
||||
async loadedGraphNode (node, app) {
|
||||
if (node.type === 'ShowTextForGPT') {
|
||||
let widget = node.widgets.filter(w => w.name == 'show_text')[0]
|
||||
|
||||
// if (widget.value) {
|
||||
// let [url, prompt] = widget.value
|
||||
|
||||
// this[`wavesurfer_${node.id}`] = updateWaveWidgetValue(
|
||||
// node.widgets,
|
||||
// node.id,
|
||||
// url,
|
||||
// prompt,
|
||||
// this[`wavesurfer_${node.id}`]
|
||||
// )
|
||||
// }
|
||||
|
||||
console.log('#loadedGraphNode', node)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1267,7 +1267,7 @@ app.registerExtension({
|
||||
})
|
||||
|
||||
widget.PictureInPicture = $el('button', {
|
||||
innerText: 'Picture In Picture',
|
||||
innerText: 'PictureInPicture',
|
||||
style: {
|
||||
display: 'pictureInPictureEnabled' in document ? 'block' : 'none',
|
||||
cursor: 'pointer',
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
import { app } from '../../../scripts/app.js'
|
||||
import { api } from '../../../scripts/api.js'
|
||||
import { ComfyWidgets } from '../../../scripts/widgets.js'
|
||||
import { $el } from '../../../scripts/ui.js'
|
||||
|
||||
import WaveSurfer from 'https://cdn.jsdelivr.net/npm/wavesurfer.js@7/dist/wavesurfer.esm.js'
|
||||
|
||||
function get_position_style (ctx, widget_width, y, node_height) {
|
||||
const MARGIN = 4 // the margin around the html element
|
||||
|
||||
/* Create a transform that deals with all the scrolling and zooming */
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(
|
||||
elRect.width / ctx.canvas.width,
|
||||
elRect.height / ctx.canvas.height
|
||||
)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(MARGIN, MARGIN + y)
|
||||
|
||||
return {
|
||||
transformOrigin: '0 0',
|
||||
transform: transform,
|
||||
left: `0`,
|
||||
top: '0',
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
maxWidth: `${widget_width - MARGIN * 2}px`,
|
||||
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
|
||||
width: `${widget_width - MARGIN * 2}px`,
|
||||
// height: `${node_height * 0.3 - MARGIN * 2}px`,
|
||||
// background: '#EEEEEE',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// alignItems: 'center',
|
||||
justifyContent: 'space-around'
|
||||
}
|
||||
}
|
||||
|
||||
//把文件转为url访问
|
||||
const parseUrl = data => {
|
||||
let { filename, subfolder, type, prompt } = data
|
||||
return {
|
||||
url: api.apiURL(
|
||||
`/view?filename=${encodeURIComponent(
|
||||
filename
|
||||
)}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
|
||||
),
|
||||
prompt
|
||||
}
|
||||
}
|
||||
|
||||
const createWaveSurfer = (wavesurfer, id,url) => {
|
||||
// Create an instance of WaveSurfer
|
||||
if (wavesurfer) {
|
||||
wavesurfer.destroy()
|
||||
}
|
||||
wavesurfer = WaveSurfer.create({
|
||||
container: '#' + id,
|
||||
waveColor: 'rgb(200, 0, 200)',
|
||||
progressColor: 'rgb(100, 0, 100)',
|
||||
// Set a bar width
|
||||
barWidth: 10,
|
||||
// Optionally, specify the spacing between bars
|
||||
barGap: 2,
|
||||
// And the bar radius
|
||||
barRadius: 6,
|
||||
url
|
||||
})
|
||||
|
||||
wavesurfer._auto = true
|
||||
|
||||
// 监听播放结束事件,重新开始播放以实现循环播放
|
||||
wavesurfer.on('finish', function () {
|
||||
// console.log(wavesurfer)
|
||||
if (wavesurfer._auto) wavesurfer.play()
|
||||
})
|
||||
|
||||
wavesurfer.on('interaction', () => {
|
||||
wavesurfer._auto = false
|
||||
if (!wavesurfer.isPlaying()) wavesurfer.play()
|
||||
})
|
||||
|
||||
// 获取当前播放时间的峰值
|
||||
wavesurfer.on('audioprocess', () => {
|
||||
if (wavesurfer.isPlaying()&&wavesurfer.getDecodedData()) {
|
||||
const channelData = wavesurfer.getDecodedData().getChannelData(0);
|
||||
const currentTime = wavesurfer.getCurrentTime()
|
||||
// console.log(wavesurfer)
|
||||
const sampleRate = wavesurfer.getDecodedData().sampleRate
|
||||
|
||||
// 定义要分析的时间窗口(例如1秒)
|
||||
const windowSize = 1
|
||||
const startSample = Math.floor(currentTime * sampleRate)
|
||||
const endSample = Math.min(
|
||||
startSample + windowSize * sampleRate,
|
||||
channelData.length
|
||||
)
|
||||
|
||||
let peak = 0
|
||||
for (let i = startSample; i < endSample; i++) {
|
||||
const value = Math.abs(channelData[i])
|
||||
if (value > peak) {
|
||||
peak = value
|
||||
}
|
||||
}
|
||||
// console.log('Current Peak:', peak)
|
||||
}
|
||||
})
|
||||
|
||||
return wavesurfer
|
||||
}
|
||||
|
||||
//更新gui
|
||||
function updateWaveWidgetValue (widgets, id, url, prompt, wavesurfer) {
|
||||
let widget = widgets.filter(w => w.name == 'AudioPlay')[0]
|
||||
// 手动更新widget值
|
||||
widget.value = [url, prompt]
|
||||
|
||||
if (widget.div) {
|
||||
widget.div.querySelector('.wave').id = `AudioPlay_${id}`
|
||||
}
|
||||
|
||||
wavesurfer = createWaveSurfer(wavesurfer, `AudioPlay_${id}`,url)
|
||||
|
||||
wavesurfer.on('ready', duration => {
|
||||
console.log('Audio duration: ' + duration + ' seconds')
|
||||
if (widget.div) {
|
||||
widget.div.setAttribute('data-url', url)
|
||||
widget.div.querySelector('.link').setAttribute('href', url)
|
||||
widget.div.querySelector(
|
||||
'.info'
|
||||
).innerHTML = `<span style="font-size: 12px;
|
||||
margin: 8px;">${duration.toFixed(
|
||||
2
|
||||
)} seconds</span> <br><span style="font-size: 14px;">${prompt||''}</span> <br>`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
wavesurfer.load(url)
|
||||
// console.log('updateWaveWidgetValue' ,url,wavesurfer)
|
||||
return wavesurfer
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'SoundLab.AudioPlay',
|
||||
async beforeRegisterNodeDef (nodeType, nodeData, app) {
|
||||
if (nodeType.comfyClass == 'AudioPlay') {
|
||||
let that = this
|
||||
// console.log('that', that)
|
||||
|
||||
const orig_nodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
orig_nodeCreated?.apply(this, arguments)
|
||||
|
||||
const widget = {
|
||||
type: 'div',
|
||||
name: 'AudioPlay',
|
||||
draw (ctx, node, widget_width, y, widget_height) {
|
||||
Object.assign(
|
||||
this.div.style,
|
||||
get_position_style(ctx, widget_width, y, node.size[1])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('AudioPlay nodeData', this)
|
||||
widget.div = $el('div', {})
|
||||
|
||||
document.body.appendChild(widget.div)
|
||||
|
||||
// wave
|
||||
const waveDiv = document.createElement('div')
|
||||
waveDiv.className = 'wave'
|
||||
waveDiv.style.minHeight = '172px'
|
||||
widget.div.appendChild(waveDiv)
|
||||
|
||||
//prompt 相关信息展示
|
||||
const infoDiv = document.createElement('div')
|
||||
infoDiv.className = 'info'
|
||||
infoDiv.style.marginBottom = '20px'
|
||||
widget.div.appendChild(infoDiv)
|
||||
|
||||
// 按钮的区域
|
||||
let btns = document.createElement('div')
|
||||
btns.className = 'btns'
|
||||
btns.style = `display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;`
|
||||
widget.div.appendChild(btns)
|
||||
|
||||
//play button
|
||||
const playBtn = document.createElement('a')
|
||||
playBtn.innerText = 'Play/Pause'
|
||||
|
||||
playBtn.style = `
|
||||
display: flex;
|
||||
padding: 4px 15px;
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;
|
||||
color: var(--descrip-text);
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s ease 0s;
|
||||
`
|
||||
|
||||
playBtn.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
if (that[`wavesurfer_${this.id}`]) {
|
||||
that[`wavesurfer_${this.id}`]?.playPause()
|
||||
that[`wavesurfer_${this.id}`]._auto = true
|
||||
}
|
||||
})
|
||||
btns.appendChild(playBtn)
|
||||
|
||||
const urlLink = document.createElement('a')
|
||||
urlLink.className = 'link'
|
||||
urlLink.innerText = 'URL'
|
||||
urlLink.setAttribute('target', '_blank')
|
||||
urlLink.style = `display: flex;
|
||||
padding: 4px 15px;
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;
|
||||
color: var(--descrip-text);
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s ease 0s;`
|
||||
// urlLink.style.minHeight = '200px'
|
||||
btns.appendChild(urlLink)
|
||||
|
||||
|
||||
//todo 导出视频 that[`wavesurfer_${this.id}`].renderer.exportImage('image/png',1,'dataURL')
|
||||
// https://github.com/diffusion-studio/ffmpeg-js
|
||||
|
||||
|
||||
this.addCustomWidget(widget)
|
||||
|
||||
const onRemoved = this.onRemoved
|
||||
this.onRemoved = () => {
|
||||
widget.div.remove()
|
||||
return onRemoved?.()
|
||||
}
|
||||
|
||||
this.size = [this.size[0], 280]
|
||||
this.serialize_widgets = true //需保存widget的值
|
||||
}
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments)
|
||||
const audio = message.audio
|
||||
console.log('#onExecuted', `AudioPlay_${this.id}`, message,audio)
|
||||
try {
|
||||
let { url, prompt } = parseUrl(audio[0])
|
||||
|
||||
that[`wavesurfer_${this.id}`] = updateWaveWidgetValue(
|
||||
this.widgets,
|
||||
this.id,
|
||||
url,
|
||||
prompt,
|
||||
that[`wavesurfer_${this.id}`]
|
||||
)
|
||||
|
||||
that[`wavesurfer_${this.id}`]?.playPause()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadedGraphNode (node, app) {
|
||||
if (node.type === 'AudioPlay') {
|
||||
let widget = node.widgets.filter(w => w.name == 'AudioPlay')[0]
|
||||
|
||||
if (widget.value) {
|
||||
let [url, prompt] = widget.value
|
||||
|
||||
this[`wavesurfer_${node.id}`] = updateWaveWidgetValue(
|
||||
node.widgets,
|
||||
node.id,
|
||||
url,
|
||||
prompt,
|
||||
this[`wavesurfer_${node.id}`]
|
||||
)
|
||||
}
|
||||
|
||||
console.log('#loadedGraphNode', node)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,323 +0,0 @@
|
||||
// touchdesigner的背景效果,把appinfo的输出,选择一张图片作为背景
|
||||
|
||||
window._bg_img = null
|
||||
|
||||
/**
|
||||
* draws the back canvas (the one containing the background and the connections)
|
||||
* @method drawBackCanvas
|
||||
**/
|
||||
LGraphCanvas.prototype.drawBackCanvas = function () {
|
||||
var canvas = this.bgcanvas
|
||||
if (
|
||||
canvas.width != this.canvas.width ||
|
||||
canvas.height != this.canvas.height
|
||||
) {
|
||||
canvas.width = this.canvas.width
|
||||
canvas.height = this.canvas.height
|
||||
}
|
||||
|
||||
if (!this.bgctx) {
|
||||
this.bgctx = this.bgcanvas.getContext('2d')
|
||||
}
|
||||
var ctx = this.bgctx
|
||||
if (ctx.start) {
|
||||
ctx.start()
|
||||
}
|
||||
|
||||
var viewport = this.viewport || [0, 0, ctx.canvas.width, ctx.canvas.height]
|
||||
|
||||
//clear
|
||||
if (this.clear_background) {
|
||||
ctx.clearRect(viewport[0], viewport[1], viewport[2], viewport[3])
|
||||
}
|
||||
|
||||
//show subgraph stack header
|
||||
if (this._graph_stack && this._graph_stack.length) {
|
||||
ctx.save()
|
||||
var parent_graph = this._graph_stack[this._graph_stack.length - 1]
|
||||
var subgraph_node = this.graph._subgraph_node
|
||||
ctx.strokeStyle = subgraph_node.bgcolor
|
||||
ctx.lineWidth = 10
|
||||
ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2)
|
||||
ctx.lineWidth = 1
|
||||
ctx.font = '40px Arial'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillStyle = subgraph_node.bgcolor || '#AAA'
|
||||
var title = ''
|
||||
for (var i = 1; i < this._graph_stack.length; ++i) {
|
||||
title += this._graph_stack[i]._subgraph_node.getTitle() + ' >> '
|
||||
}
|
||||
ctx.fillText(title + subgraph_node.getTitle(), canvas.width * 0.5, 40)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
var bg_already_painted = false
|
||||
if (this.onRenderBackground) {
|
||||
bg_already_painted = this.onRenderBackground(canvas, ctx)
|
||||
}
|
||||
|
||||
//reset in case of error
|
||||
if (!this.viewport) {
|
||||
ctx.restore()
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||
}
|
||||
this.visible_links.length = 0
|
||||
|
||||
if (this.graph) {
|
||||
//apply transformations
|
||||
ctx.save()
|
||||
this.ds.toCanvasContext(ctx)
|
||||
|
||||
//render BG
|
||||
if (
|
||||
this.ds.scale < 1 &&
|
||||
!bg_already_painted &&
|
||||
this.clear_background_color
|
||||
) {
|
||||
ctx.fillStyle = this.clear_background_color
|
||||
ctx.fillRect(
|
||||
this.visible_area[0],
|
||||
this.visible_area[1],
|
||||
this.visible_area[2],
|
||||
this.visible_area[3]
|
||||
)
|
||||
}
|
||||
|
||||
// 主要修改
|
||||
if (this.background_image && this.ds.scale > 0.5 && !bg_already_painted) {
|
||||
if (this.zoom_modify_alpha) {
|
||||
//使得 alpha 越接近0时变化越缓慢。
|
||||
let alpha = (1.0 - 0.5 / this.ds.scale) * this.editor_alpha
|
||||
ctx.globalAlpha = Math.min(Math.max(0, Math.sqrt(alpha)), 1)
|
||||
// console.log((1.0 - 0.5 / this.ds.scale) * this.editor_alpha)
|
||||
} else {
|
||||
ctx.globalAlpha = this.editor_alpha
|
||||
}
|
||||
ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = false // ctx.mozImageSmoothingEnabled =
|
||||
if (!this._bg_img || this._bg_img.name != this.background_image) {
|
||||
this._bg_img = new Image()
|
||||
this._bg_img.name = this.background_image
|
||||
this._bg_img.src = this.background_image
|
||||
var that = this
|
||||
this._bg_img.onload = function () {
|
||||
that.draw(true, true)
|
||||
}
|
||||
}
|
||||
|
||||
var pattern = null
|
||||
if (this._pattern == null && this._bg_img.width > 0) {
|
||||
pattern = ctx.createPattern(this._bg_img, 'repeat')
|
||||
this._pattern_img = this._bg_img
|
||||
this._pattern = pattern
|
||||
} else {
|
||||
pattern = this._pattern
|
||||
}
|
||||
|
||||
if (pattern) {
|
||||
ctx.fillStyle = pattern
|
||||
ctx.fillRect(
|
||||
this.visible_area[0],
|
||||
this.visible_area[1],
|
||||
this.visible_area[2],
|
||||
this.visible_area[3]
|
||||
)
|
||||
ctx.fillStyle = 'transparent'
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1.0
|
||||
ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = true //= ctx.mozImageSmoothingEnabled
|
||||
}
|
||||
|
||||
//groups
|
||||
if (this.graph._groups.length && !this.live_mode) {
|
||||
this.drawGroups(canvas, ctx)
|
||||
}
|
||||
|
||||
if (this.onDrawBackground) {
|
||||
this.onDrawBackground(ctx, this.visible_area)
|
||||
}
|
||||
if (this.onBackgroundRender) {
|
||||
//LEGACY
|
||||
console.error(
|
||||
'WARNING! onBackgroundRender deprecated, now is named onDrawBackground '
|
||||
)
|
||||
this.onBackgroundRender = null
|
||||
}
|
||||
|
||||
//DEBUG: show clipping area
|
||||
//ctx.fillStyle = "red";
|
||||
//ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20);
|
||||
|
||||
//bg
|
||||
if (this.render_canvas_border) {
|
||||
ctx.strokeStyle = '#235'
|
||||
ctx.strokeRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
if (this.render_connections_shadows) {
|
||||
ctx.shadowColor = '#000'
|
||||
ctx.shadowOffsetX = 0
|
||||
ctx.shadowOffsetY = 0
|
||||
ctx.shadowBlur = 6
|
||||
} else {
|
||||
ctx.shadowColor = 'rgba(0,0,0,0)'
|
||||
}
|
||||
|
||||
//draw connections
|
||||
if (!this.live_mode) {
|
||||
this.drawConnections(ctx)
|
||||
}
|
||||
|
||||
ctx.shadowColor = 'rgba(0,0,0,0)'
|
||||
|
||||
//restore state
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (ctx.finish) {
|
||||
ctx.finish()
|
||||
}
|
||||
|
||||
this.dirty_bgcanvas = false
|
||||
this.dirty_canvas = true //to force to repaint the front canvas with the bgcanvas
|
||||
}
|
||||
|
||||
function imgToCanvasBase64 (img) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
ctx.drawImage(img, 0, 0)
|
||||
const base64 = canvas.toDataURL('image/png')
|
||||
|
||||
return base64
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
function convertImageToBase64 (img) {
|
||||
// const img = new Image()
|
||||
// img.src = 'path/to/your/image.jpg' // 替换为你的图片路径
|
||||
// console.log('convertImageToBase64',img)
|
||||
try {
|
||||
const base64 = imgToCanvasBase64(img)
|
||||
return base64
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
function getInputsAndOutputs () {
|
||||
const outputs =
|
||||
`PreviewImage,SaveImage,TransparentImage,VHS_VideoCombine,VideoCombine_Adv,Image Save,SaveImageAndMetadata_`.split(
|
||||
','
|
||||
)
|
||||
|
||||
let outputsId = []
|
||||
|
||||
for (let node of app.graph._nodes) {
|
||||
if (outputs.includes(node.type)) {
|
||||
outputsId.push(node.id)
|
||||
}
|
||||
}
|
||||
|
||||
return outputsId
|
||||
}
|
||||
|
||||
function getRandomElement (arr) {
|
||||
const randomIndex = Math.floor(Math.random() * arr.length)
|
||||
return arr[randomIndex]
|
||||
}
|
||||
|
||||
async function getBG () {
|
||||
var outputs = []
|
||||
|
||||
for (let id of app.graph
|
||||
.getNodeById(50)
|
||||
.widgets.filter(w => w.name === 'output_ids')[0]
|
||||
.value.split('\n')) {
|
||||
if (getInputsAndOutputs().map(Number).includes(Number(id))) {
|
||||
if (app.graph.getNodeById(id).imgs && app.graph.getNodeById(id).imgs[0]) {
|
||||
let b = convertImageToBase64(app.graph.getNodeById(id).imgs[0])
|
||||
// console.log(b)
|
||||
outputs.push(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var BACKGROUND_IMAGE = getRandomElement(outputs),
|
||||
CLEAR_BACKGROUND_COLOR = 'rgba(0,0,0,0.9)'
|
||||
|
||||
if (!window._bg_img) {
|
||||
window._bg_img = app.canvas._bg_img.src
|
||||
}
|
||||
// let img=new Image();
|
||||
// img.src=BACKGROUND_IMAGE;
|
||||
|
||||
//去掉透明度过度
|
||||
// app.canvas.zoom_modify_alpha=false;
|
||||
//整体透明度
|
||||
app.canvas.editor_alpha = 1.1
|
||||
// app.canvas._pattern=ctx.createPattern(img, "no-repeat");
|
||||
app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR)
|
||||
app.canvas.draw(true, true)
|
||||
}
|
||||
|
||||
class BgRunner {
|
||||
constructor () {
|
||||
this.intervalId = null
|
||||
this.running = false
|
||||
}
|
||||
|
||||
// 要运行的方法
|
||||
bg () {
|
||||
console.log('方法bg正在运行')
|
||||
getBG()
|
||||
}
|
||||
|
||||
// 启动bg方法每秒运行一次
|
||||
start () {
|
||||
if (!this.running) {
|
||||
this.intervalId = setInterval(() => this.bg(), 1500)
|
||||
this.running = true
|
||||
}
|
||||
}
|
||||
|
||||
// 停止bg方法的运行
|
||||
stop () {
|
||||
if (this.running) {
|
||||
clearInterval(this.intervalId)
|
||||
this.intervalId = null
|
||||
this.running = false
|
||||
|
||||
if (window._bg_img) {
|
||||
var BACKGROUND_IMAGE = window._bg_img,
|
||||
CLEAR_BACKGROUND_COLOR = 'rgba(0,0,0,1)'
|
||||
app.canvas.editor_alpha = 1
|
||||
|
||||
app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR)
|
||||
app.canvas.draw(true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换start和stop
|
||||
toggle () {
|
||||
if (this.running) {
|
||||
this.stop()
|
||||
} else {
|
||||
this.start()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取运行状态
|
||||
isRunning () {
|
||||
return this.running
|
||||
}
|
||||
}
|
||||
|
||||
// 示例用法
|
||||
// const runner = new BgRunner();
|
||||
// runner.start();
|
||||
// setTimeout(() => runner.stop(), 5000);
|
||||
|
||||
export const td_bg = new BgRunner()
|
||||
@@ -163,7 +163,7 @@ async function createMenu () {
|
||||
|
||||
// appsButton.onclick = () =>
|
||||
appsButton.onclick = async () => {
|
||||
if (window._mixlab_llamacpp&&window._mixlab_llamacpp.model&&window._mixlab_llamacpp.model.length>0) {
|
||||
if (window._mixlab_llamacpp) {
|
||||
//显示运行的模型
|
||||
createModelsModal([
|
||||
window._mixlab_llamacpp.url,
|
||||
@@ -800,11 +800,11 @@ async function fetchReadmeContent (url) {
|
||||
|
||||
async function startLLM (model) {
|
||||
let res = await start_llama(model)
|
||||
window._mixlab_llamacpp = res||{ model:[] }
|
||||
window._mixlab_llamacpp = res
|
||||
|
||||
localStorage.setItem('_mixlab_llama_select', res?.model||'')
|
||||
localStorage.setItem('_mixlab_llama_select', res.model)
|
||||
|
||||
if (document.body.querySelector('#mixlab_chatbot_by_llamacpp')&&window._mixlab_llamacpp?.url) {
|
||||
if (document.body.querySelector('#mixlab_chatbot_by_llamacpp')&&window._mixlab_llamacpp.url) {
|
||||
document.body
|
||||
.querySelector('#mixlab_chatbot_by_llamacpp')
|
||||
.setAttribute('title', window._mixlab_llamacpp.url)
|
||||
@@ -1029,7 +1029,7 @@ function createModelsModal (models) {
|
||||
|
||||
modalContent.appendChild(input)
|
||||
|
||||
if (!window._mixlab_llamacpp||(window._mixlab_llamacpp?.model?.length==0)) {
|
||||
if (!window._mixlab_llamacpp) {
|
||||
for (const m of models) {
|
||||
let d = document.createElement('div')
|
||||
d.innerText = `${showTextByLanguage('Run', {
|
||||
@@ -1439,14 +1439,10 @@ app.registerExtension({
|
||||
|
||||
LGraphCanvas.prototype.fixTheNode = function (node) {
|
||||
let new_node = LiteGraph.createNode(node.comfyClass)
|
||||
console.log(node)
|
||||
if(new_node){
|
||||
new_node.pos = [node.pos[0], node.pos[1]]
|
||||
app.canvas.graph.add(new_node, false)
|
||||
copyNodeValues(node, new_node)
|
||||
app.canvas.graph.remove(node)
|
||||
}
|
||||
|
||||
new_node.pos = [node.pos[0], node.pos[1]]
|
||||
app.canvas.graph.add(new_node, false)
|
||||
copyNodeValues(node, new_node)
|
||||
app.canvas.graph.remove(node)
|
||||
}
|
||||
|
||||
smart_init()
|
||||
@@ -1788,7 +1784,6 @@ app.registerExtension({
|
||||
{
|
||||
content: 'Help ♾️Mixlab', // with a name
|
||||
callback: () => {
|
||||
// console.log('#data',node)
|
||||
LGraphCanvas.prototype.helpAboutNode(node)
|
||||
} // and the callback
|
||||
},
|
||||
|
||||
@@ -6,6 +6,8 @@ import { $el } from '../../../scripts/ui.js'
|
||||
|
||||
// The code is based on ComfyUI-VideoHelperSuite modification.
|
||||
|
||||
|
||||
|
||||
function injectCSS (css) {
|
||||
// 检查页面中是否已经存在具有相同内容的style标签
|
||||
const existingStyle = document.querySelector('style')
|
||||
@@ -238,7 +240,15 @@ app.registerExtension({
|
||||
}
|
||||
})
|
||||
|
||||
function offsetDOMWidget (widget, ctx, node, widgetWidth, widgetY, height) {
|
||||
|
||||
function offsetDOMWidget(
|
||||
widget,
|
||||
ctx,
|
||||
node,
|
||||
widgetWidth,
|
||||
widgetY,
|
||||
height
|
||||
) {
|
||||
const margin = 10
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
@@ -260,18 +270,18 @@ function offsetDOMWidget (widget, ctx, node, widgetWidth, widgetY, height) {
|
||||
position: 'absolute',
|
||||
background: !node.color ? '' : node.color,
|
||||
color: !node.color ? '' : 'white',
|
||||
zIndex: 5 //app.graph._nodes.indexOf(node),
|
||||
zIndex: 5, //app.graph._nodes.indexOf(node),
|
||||
})
|
||||
}
|
||||
|
||||
export const hasWidgets = node => {
|
||||
export const hasWidgets = (node) => {
|
||||
if (!node.widgets || !node.widgets?.[Symbol.iterator]) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const cleanupNode = node => {
|
||||
export const cleanupNode = (node) => {
|
||||
if (!hasWidgets(node)) {
|
||||
return
|
||||
}
|
||||
@@ -288,43 +298,43 @@ export const cleanupNode = node => {
|
||||
}
|
||||
}
|
||||
|
||||
const createPreviewElement = (name, val, format) => {
|
||||
const [type] = format.split('/')
|
||||
const CreatePreviewElement = (name, val, format) => {
|
||||
const [type] = format.split('/')
|
||||
const w = {
|
||||
name,
|
||||
type,
|
||||
value: val,
|
||||
draw: function (ctx, node, widgetWidth, widgetY, height) {
|
||||
const [cw, ch] = this.computeSize(widgetWidth)
|
||||
offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
|
||||
},
|
||||
computeSize: function (_) {
|
||||
const ratio = this.inputRatio || 1
|
||||
const width = Math.max(220, this.parent.size[0])
|
||||
return [width, width / ratio + 10]
|
||||
},
|
||||
onRemoved: function () {
|
||||
if (this.inputEl) {
|
||||
this.inputEl.remove()
|
||||
}
|
||||
name,
|
||||
type,
|
||||
value: val,
|
||||
draw: function (ctx, node, widgetWidth, widgetY, height) {
|
||||
const [cw, ch] = this.computeSize(widgetWidth)
|
||||
offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
|
||||
},
|
||||
computeSize: function (_) {
|
||||
const ratio = this.inputRatio || 1
|
||||
const width = Math.max(220, this.parent.size[0])
|
||||
return [width, (width / ratio + 10)]
|
||||
},
|
||||
onRemoved: function () {
|
||||
if (this.inputEl) {
|
||||
this.inputEl.remove()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
w.inputEl = document.createElement(type === 'video' ? 'video' : 'img')
|
||||
w.inputEl.src = w.value
|
||||
if (type === 'video') {
|
||||
w.inputEl.setAttribute('type', 'video/webm');
|
||||
w.inputEl.autoplay = true
|
||||
w.inputEl.loop = true
|
||||
w.inputEl.controls = false;
|
||||
}
|
||||
w.inputEl.onload = function () {
|
||||
w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight
|
||||
}
|
||||
document.body.appendChild(w.inputEl)
|
||||
return w
|
||||
}
|
||||
|
||||
w.inputEl = document.createElement(type === 'video' ? 'video' : 'img')
|
||||
w.inputEl.src = w.value
|
||||
|
||||
if (type === 'video' || format.match('.mp4')) {
|
||||
w.inputEl.setAttribute('type', 'video/webm')
|
||||
w.inputEl.autoplay = true
|
||||
w.inputEl.loop = true
|
||||
w.inputEl.controls = true
|
||||
}
|
||||
w.inputEl.onload = function () {
|
||||
w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight
|
||||
}
|
||||
document.body.appendChild(w.inputEl)
|
||||
return w
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Mixlab.Video.ImageListReplace',
|
||||
@@ -459,10 +469,7 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
nodeData?.name == 'VideoCombine_Adv' ||
|
||||
nodeData?.name == 'CombineAudioVideo'
|
||||
) {
|
||||
if (nodeData?.name == 'VideoCombine_Adv') {
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
const prefix = 'vhs_gif_preview_'
|
||||
@@ -482,7 +489,7 @@ app.registerExtension({
|
||||
'/view?' + new URLSearchParams(params).toString()
|
||||
)
|
||||
const w = this.addCustomWidget(
|
||||
createPreviewElement(
|
||||
CreatePreviewElement(
|
||||
`${prefix}_${i}`,
|
||||
previewUrl,
|
||||
params.format || 'image/gif'
|
||||
|
||||
+525
@@ -0,0 +1,525 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
VoiceStreamAI Client Interface
|
||||
Real-time audio transcription using self-hosted Whisper and WebSocket
|
||||
|
||||
Contributor:
|
||||
- Alessandro Saccoia - alessandro.saccoia@gmail.com
|
||||
-->
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Audio Stream to WebSocket Server</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f4f4f4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin: 20px auto;
|
||||
padding: 10px;
|
||||
width: 80%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controls input,
|
||||
.controls button,
|
||||
.controls select {
|
||||
padding: 8px;
|
||||
margin: 5px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#transcription {
|
||||
margin: 20px auto;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
width: 80%;
|
||||
height: 150px;
|
||||
overflow-y: auto;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.buffering-strategy-panel {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* ... existing styles ... */
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/* background-color: #333;
|
||||
color:white */
|
||||
}
|
||||
|
||||
#mic_container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#mic {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-top: 1rem;
|
||||
|
||||
width: 300px
|
||||
}
|
||||
|
||||
#asistant {
|
||||
width: 300px;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
overflow-y: scroll;
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="mic_container">
|
||||
<div id="mic"></div>
|
||||
<div id="asistant"></div>
|
||||
</div>
|
||||
|
||||
<h1>VAD + Whisper + </h1>
|
||||
|
||||
<button id="init_server">Server</button>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label class="label" for="websocketAddress">WebSocket Address:</label>
|
||||
<input type="text" id="websocketAddress" value="ws://localhost:8725">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="label" for="bufferingStrategySelect" onchange="toggleBufferingStrategyPanel()">Buffering
|
||||
Strategy:</label>
|
||||
<select id="bufferingStrategySelect">
|
||||
<option value="silence_at_end_of_chunk" selected>Silence at End of Chunk</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="silence_at_end_of_chunk_options_panel">
|
||||
<div class="control-group">
|
||||
<label class="label" for="chunk_length_seconds">Chunk Length (s):</label>
|
||||
<input type="number" id="chunk_length_seconds" value="3" min="1">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="label" for="chunk_offset_seconds">Silence at the End of Chunk (s):</label>
|
||||
<input type="number" id="chunk_offset_seconds" value="0.1" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="label" for="languageSelect">Language:</label>
|
||||
<select id="languageSelect">
|
||||
<option value="multilingual">Multilingual</option>
|
||||
<option value="english">English</option>
|
||||
<option value="italian">Italian</option>
|
||||
<option value="spanish">Spanish</option>
|
||||
<option value="french">French</option>
|
||||
<option value="german">German</option>
|
||||
<option value="chinese">Chinese</option>
|
||||
<option value="arabic">Arabic</option>
|
||||
<option value="portuguese">Portuguese</option>
|
||||
<option value="russian">Russian</option>
|
||||
<option value="japanese">Japanese</option>
|
||||
<option value="dutch">Dutch</option>
|
||||
<option value="korean">Korean</option>
|
||||
<option value="hindi">Hindi</option>
|
||||
<option value="turkish">Turkish</option>
|
||||
<option value="swedish">Swedish</option>
|
||||
<option value="norwegian">Norwegian</option>
|
||||
<option value="danish">Danish</option>
|
||||
<option value="polish">Polish</option>
|
||||
<option value="finnish">Finnish</option>
|
||||
<option value="thai">Thai</option>
|
||||
<option value="czech">Czech</option>
|
||||
<option value="hungarian">Hungarian</option>
|
||||
<option value="greek">Greek</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="connectButton">Connect</button>
|
||||
</div>
|
||||
<button id="startButton" disabled>Start Streaming</button>
|
||||
<button id="stopButton" disabled>Stop Streaming</button>
|
||||
<div id="transcription"></div>
|
||||
<br />
|
||||
<div>WebSocket: <span id="webSocketStatus">Not Connected</span></div>
|
||||
<div>Detected Language: <span id="detected_language">Undefined</span></div>
|
||||
<div>Last Processing Time: <span id="processing_time">Undefined</span></div>
|
||||
|
||||
<script type="module">
|
||||
|
||||
// Record plugin
|
||||
|
||||
import WaveSurfer from 'https://cdn.jsdelivr.net/npm/wavesurfer.js@7/dist/wavesurfer.esm.js'
|
||||
import RecordPlugin from 'https://cdn.jsdelivr.net/npm/wavesurfer.js/dist/plugins/record.esm.js'
|
||||
|
||||
// 可视化
|
||||
let wavesurfer, record;
|
||||
|
||||
// ws服务
|
||||
let websocket;
|
||||
let context;
|
||||
let processor;
|
||||
let globalStream;
|
||||
|
||||
const websocket_uri = 'ws://localhost:8765';
|
||||
const bufferSize = 4096;
|
||||
let isRecording = false;
|
||||
let chunk_length_seconds, chunk_offset_seconds, language;
|
||||
|
||||
// Record button
|
||||
const startButton = document.getElementById('startButton'),
|
||||
stopButton = document.getElementById('stopButton'),
|
||||
connectButton = document.getElementById('connectButton'),
|
||||
initServerButton = document.getElementById('init_server')
|
||||
|
||||
const createWaveSurfer = () => {
|
||||
// Create an instance of WaveSurfer
|
||||
if (wavesurfer) {
|
||||
wavesurfer.destroy()
|
||||
}
|
||||
wavesurfer = WaveSurfer.create({
|
||||
container: '#mic',
|
||||
waveColor: 'rgb(200, 0, 200)',
|
||||
progressColor: 'rgb(100, 0, 100)',
|
||||
renderFunction: (channels, ctx) => {
|
||||
const { width, height } = ctx.canvas
|
||||
// console.log(width, height)
|
||||
const scale = channels[0].length / width
|
||||
const step = 20
|
||||
|
||||
ctx.translate(0, height / 2)
|
||||
ctx.strokeStyle = ctx.fillStyle
|
||||
ctx.beginPath()
|
||||
|
||||
for (let i = 0; i < width; i += step * 2) {
|
||||
const index = Math.floor(i * scale)
|
||||
const value = Math.abs(channels[0][index])
|
||||
let x = i
|
||||
let y = value * height * 1.2
|
||||
|
||||
ctx.moveTo(x, 0)
|
||||
ctx.lineTo(x, y)
|
||||
ctx.arc(x + step / 2, y, step / 2, Math.PI, 0, true)
|
||||
ctx.lineTo(x + step, 0)
|
||||
|
||||
x = x + step
|
||||
y = -y
|
||||
ctx.moveTo(x, 0)
|
||||
ctx.lineTo(x, y)
|
||||
ctx.arc(x + step / 2, y, step / 2, Math.PI, 0, false)
|
||||
ctx.lineTo(x + step, 0)
|
||||
}
|
||||
|
||||
ctx.stroke()
|
||||
ctx.closePath()
|
||||
},
|
||||
// // Set a bar width
|
||||
// barWidth: 20,
|
||||
// // Optionally, specify the spacing between bars
|
||||
// barGap: 4,
|
||||
// // And the bar radius
|
||||
// barRadius: 2,
|
||||
})
|
||||
|
||||
// Initialize the Record plugin
|
||||
record = wavesurfer.registerPlugin(RecordPlugin.create({ scrollingWaveform: false, renderRecordedAudio: false }))
|
||||
// Render recorded audio
|
||||
// recButton.textContent = 'Record'
|
||||
|
||||
}
|
||||
|
||||
startButton.addEventListener('click', e => {
|
||||
record.startRecording()
|
||||
|
||||
if (isRecording) return;
|
||||
isRecording = true;
|
||||
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
context = new AudioContext();
|
||||
|
||||
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
|
||||
globalStream = stream;
|
||||
const input = context.createMediaStreamSource(stream);
|
||||
processor = context.createScriptProcessor(bufferSize, 1, 1);
|
||||
processor.onaudioprocess = e => processAudio(e);
|
||||
input.connect(processor);
|
||||
processor.connect(context.destination);
|
||||
|
||||
sendAudioConfig();
|
||||
}).catch(error => console.error('Error accessing microphone', error));
|
||||
|
||||
// Disable start button and enable stop button
|
||||
startButton.disabled = true;
|
||||
stopButton.disabled = false;
|
||||
|
||||
});
|
||||
|
||||
stopButton.addEventListener('click', e => {
|
||||
if (!isRecording) return;
|
||||
isRecording = false;
|
||||
|
||||
if (globalStream) {
|
||||
globalStream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
if (processor) {
|
||||
processor.disconnect();
|
||||
processor = null;
|
||||
}
|
||||
if (context) {
|
||||
context.close().then(() => context = null);
|
||||
}
|
||||
startButton.disabled = false;
|
||||
stopButton.disabled = true;
|
||||
|
||||
// 可视化部分
|
||||
if (record.isRecording() || record.isPaused()) {
|
||||
record.stopRecording()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
connectButton.addEventListener('click', e => {
|
||||
initWebSocket()
|
||||
})
|
||||
|
||||
initServerButton.addEventListener('click', async e => {
|
||||
const response = await fetch('/mixlab/start_live', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
port: 8323,
|
||||
model: ''
|
||||
})
|
||||
})
|
||||
console.log(await response.json())
|
||||
|
||||
})
|
||||
|
||||
// Initialize on page load
|
||||
window.onload = () => {
|
||||
initWebSocket()
|
||||
createWaveSurfer();
|
||||
};
|
||||
|
||||
function initWebSocket() {
|
||||
const websocketAddress = document.getElementById('websocketAddress').value;
|
||||
chunk_length_seconds = document.getElementById('chunk_length_seconds').value;
|
||||
chunk_offset_seconds = document.getElementById('chunk_offset_seconds').value;
|
||||
const selectedLanguage = document.getElementById('languageSelect').value;
|
||||
language = selectedLanguage !== 'multilingual' ? selectedLanguage : null;
|
||||
|
||||
if (!websocketAddress) {
|
||||
console.log("WebSocket address is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (websocket) websocket.close()
|
||||
|
||||
websocket = new WebSocket(websocketAddress);
|
||||
websocket.onopen = () => {
|
||||
console.log("WebSocket connection established");
|
||||
document.getElementById("webSocketStatus").textContent = 'Connected';
|
||||
startButton.disabled = false;
|
||||
};
|
||||
websocket.onclose = event => {
|
||||
console.log("WebSocket connection closed", event);
|
||||
document.getElementById("webSocketStatus").textContent = 'Not Connected';
|
||||
stopButton.click()
|
||||
startButton.disabled = true;
|
||||
stopButton.disabled = true;
|
||||
|
||||
// setTimeout(()=>initWebSocket(),1000)
|
||||
};
|
||||
websocket.onmessage = event => {
|
||||
console.log("Message from server:", event.data);
|
||||
const transcript_data = JSON.parse(event.data);
|
||||
|
||||
if (transcript_data.status === 'chat_start') {
|
||||
updateTranscription(transcript_data);
|
||||
stopButton.click();
|
||||
} else if (transcript_data.status === 'chat_end') {
|
||||
let asistant = decodeURIComponent(transcript_data.asistant)
|
||||
document.getElementById('asistant').innerText = asistant;
|
||||
startButton.click();
|
||||
}
|
||||
|
||||
};
|
||||
websocket.onerror = () => {
|
||||
// setTimeout(()=>initWebSocket(),1000)
|
||||
}
|
||||
}
|
||||
|
||||
function updateTranscription(transcript_data) {
|
||||
const transcriptionDiv = document.getElementById('transcription');
|
||||
const languageDiv = document.getElementById('detected_language');
|
||||
|
||||
if (transcript_data['words'] && transcript_data['words'].length > 0) {
|
||||
// Append words with color based on their probability
|
||||
transcript_data['words'].forEach(wordData => {
|
||||
const span = document.createElement('span');
|
||||
const probability = wordData['probability'];
|
||||
span.textContent = wordData['word'] + ' ';
|
||||
|
||||
// Set the color based on the probability
|
||||
if (probability > 0.9) {
|
||||
span.style.color = 'green';
|
||||
} else if (probability > 0.6) {
|
||||
span.style.color = 'orange';
|
||||
} else {
|
||||
span.style.color = 'red';
|
||||
}
|
||||
|
||||
transcriptionDiv.appendChild(span);
|
||||
});
|
||||
|
||||
// Add a new line at the end
|
||||
transcriptionDiv.appendChild(document.createElement('br'));
|
||||
} else {
|
||||
// Fallback to plain text
|
||||
transcriptionDiv.textContent += transcript_data['text'] + '\n';
|
||||
}
|
||||
|
||||
// Update the language information
|
||||
if (transcript_data['language'] && transcript_data['language_probability']) {
|
||||
languageDiv.textContent = transcript_data['language'] + ' (' + transcript_data['language_probability'].toFixed(2) + ')';
|
||||
}
|
||||
|
||||
// Update the processing time, if available
|
||||
const processingTimeDiv = document.getElementById('processing_time');
|
||||
if (transcript_data['processing_time']) {
|
||||
processingTimeDiv.textContent = 'Processing time: ' + transcript_data['processing_time'].toFixed(2) + ' seconds';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function sendAudioConfig() {
|
||||
let selectedStrategy = document.getElementById('bufferingStrategySelect').value;
|
||||
let processingArgs = {};
|
||||
|
||||
if (selectedStrategy === 'silence_at_end_of_chunk') {
|
||||
processingArgs = {
|
||||
chunk_length_seconds: parseFloat(document.getElementById('chunk_length_seconds').value),
|
||||
chunk_offset_seconds: parseFloat(document.getElementById('chunk_offset_seconds').value)
|
||||
};
|
||||
}
|
||||
|
||||
const audioConfig = {
|
||||
type: 'config',
|
||||
data: {
|
||||
sampleRate: context.sampleRate,
|
||||
bufferSize: bufferSize,
|
||||
channels: 1, // Assuming mono channel
|
||||
language: language,
|
||||
processing_strategy: selectedStrategy,
|
||||
processing_args: processingArgs
|
||||
}
|
||||
};
|
||||
|
||||
websocket.send(JSON.stringify(audioConfig));
|
||||
}
|
||||
|
||||
function downsampleBuffer(buffer, inputSampleRate, outputSampleRate) {
|
||||
if (inputSampleRate === outputSampleRate) {
|
||||
return buffer;
|
||||
}
|
||||
var sampleRateRatio = inputSampleRate / outputSampleRate;
|
||||
var newLength = Math.round(buffer.length / sampleRateRatio);
|
||||
var result = new Float32Array(newLength);
|
||||
var offsetResult = 0;
|
||||
var offsetBuffer = 0;
|
||||
while (offsetResult < result.length) {
|
||||
var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
|
||||
var accum = 0, count = 0;
|
||||
for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
|
||||
accum += buffer[i];
|
||||
count++;
|
||||
}
|
||||
result[offsetResult] = accum / count;
|
||||
offsetResult++;
|
||||
offsetBuffer = nextOffsetBuffer;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function processAudio(e) {
|
||||
const inputSampleRate = context.sampleRate;
|
||||
const outputSampleRate = 16000; // Target sample rate
|
||||
|
||||
const left = e.inputBuffer.getChannelData(0);
|
||||
const downsampledBuffer = downsampleBuffer(left, inputSampleRate, outputSampleRate);
|
||||
const audioData = convertFloat32ToInt16(downsampledBuffer);
|
||||
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.send(audioData);
|
||||
}
|
||||
}
|
||||
|
||||
function convertFloat32ToInt16(buffer) {
|
||||
let l = buffer.length;
|
||||
const buf = new Int16Array(l);
|
||||
while (l--) {
|
||||
buf[l] = Math.min(1, buffer[l]) * 0x7FFF;
|
||||
}
|
||||
return buf.buffer;
|
||||
}
|
||||
|
||||
|
||||
function toggleBufferingStrategyPanel() {
|
||||
var selectedStrategy = document.getElementById('bufferingStrategySelect').value;
|
||||
if (selectedStrategy === 'silence_at_end_of_chunk') {
|
||||
var panel = document.getElementById('silence_at_end_of_chunk_options_panel');
|
||||
panel.classList.remove('hidden');
|
||||
} else {
|
||||
var panel = document.getElementById('silence_at_end_of_chunk_options_panel');
|
||||
panel.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+439
-675
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user