Compare commits

...
7 Commits
Author SHA1 Message Date
shadowcz007 8ded4829f3 0.40.0 2024-08-23 12:31:16 +08:00
shadowcz007 c4b6acb916 fixbug 2024-08-23 12:27:53 +08:00
shadowcz007 9beb81c303 Update README.md 2024-08-23 12:04:52 +08:00
shadowcz007 f8dd4c6efa node-not-found 2024-08-23 12:00:41 +08:00
shadowcz007 6ce5aa6a3a Enhanced
Enhanced navigation to GitHub
右键菜单支持 text-to-text,方便对 prompt 词补全,支持云LLM或者是本地LLM。
2024-08-23 10:45:23 +08:00
shadowcz007 d2efa8a90a Update requirements.txt 2024-08-22 20:48:43 +08:00
shadowcz007 c6374063e9 Update MiniCPMNode.py 2024-08-22 20:48:17 +08:00
14 changed files with 398 additions and 606 deletions
+20 -5
View File
@@ -8,6 +8,8 @@ For business cooperation, please contact email 389570357@qq.com
##### `最新`:
- 右键菜单支持 text-to-text,方便对 prompt 词补全,支持云LLM或者是本地LLM。
- 增加 MiniCPM-V 2.6 int4
This is the int4 quantized version of MiniCPM-V 2.6.
@@ -29,7 +31,6 @@ Running with int4 version would use lower GPU memory (about 7GB).
<!-- - ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。模型下载后,放置到 `models/llamafile/` -->
<!-- - 右键菜单支持 text-to-text,方便对 prompt 词补全 -->
<!--
强烈推荐:
[Phi-3-mini-4k-instruct-function-calling-GGUF](https://huggingface.co/nold/Phi-3-mini-4k-instruct-function-calling-GGUF)
@@ -291,17 +292,31 @@ from [simple-lama-inpainting](https://github.com/enesmsahin/simple-lama-inpainti
**_ briarmbg _** model was developed by BRlA Al and can be used as an open-source model for non-commercial purposes
### Improvement
### Enhancement
- Add "help" option to the context menu for each node.
- Add "Nodes Map" option to the global context menu.
- Direct "Help" option accessible through node context menu.
An improvement has been made to directly redirect to GitHub to search for missing nodes when loading the graph.
- "Nodes Map" feature added to global context menu.
- An improvement has been made to directly redirect to GitHub to search for missing nodes when loading the graph.
*** If not needed, you can comment out ```app.showMissingNodesError``` in the ```ui_mixlab.js``` file.
![help](./assets/help.png)
![node-not-found](./assets/node-not-found.png)
- Right-click shortcut
右键菜单支持 text-to-text,方便对 prompt 词补全,支持云LLM或者是本地LLM。
The right-click menu supports text-to-text conversion, facilitating prompt word completion, and supports cloud LLMs or local LLMs.
Local LLM API example:```http://localhost:1234/v1```
![alt text](./assets/1724380841822.png)
### Models
- [Download TripoSR](https://huggingface.co/stabilityai/TripoSR/blob/main/model.ckpt) and place it in `models/triposr`
+22 -124
View File
@@ -620,24 +620,33 @@ async def chat_completions(request):
data = await request.json()
messages = data.get('messages')
key=data.get('key')
api_url=data.get("api_url")
model_name=data.get("model_name")
if not api_url:
api_url="https://api.siliconflow.cn/v1"
if not model_name:
model_name="01-ai/Yi-1.5-9B-Chat-16K"
if not messages:
return web.json_response({"error": "No messages provided"}, status=400)
async def generate():
try:
client=openai_client(key,"https://api.siliconflow.cn/v1")
response = client.chat.completions.create(
model="01-ai/Yi-1.5-9B-Chat-16K",
messages=messages,
stream=True
)
for chunk in response:
if hasattr(chunk.choices[0].delta, 'content'):
content = chunk.choices[0].delta.content
if content is not None:
yield content.encode('utf-8') + b"\r\n"
headers = {
'Authorization': f'Bearer {key}',
'Content-Type': 'application/json'
}
payload = {
'model': model_name,
'messages': messages,
'stream': True
}
async with aiohttp.ClientSession() as session:
async with session.post(f'{api_url}/chat/completions', json=payload, headers=headers) as resp:
async for line in resp.content:
yield line
except Exception as e:
yield f"Error: {str(e)}".encode('utf-8') + b"\r\n"
@@ -971,117 +980,6 @@ async def handle_ar_page(request):
return web.Response(text="HTML file not found", status=404)
# async def start_local_llm(data):
# global llama_port,llama_model,llama_chat_format
# if llama_port and llama_model and llama_chat_format:
# return {"port":llama_port,"model":llama_model,"chat_format":llama_chat_format}
# import threading
# import uvicorn
# from llama_cpp.server.app import create_app
# from llama_cpp.server.settings import (
# Settings,
# ServerSettings,
# ModelSettings,
# ConfigFileSettings,
# )
# if not "model" in data and "model_path" in data:
# data['model']= os.path.basename(data["model_path"])
# model=data["model_path"]
# elif "model" in data:
# model=get_llama_model_path(data['model'])
# n_gpu_layers=-1
# if "n_gpu_layers" in data:
# n_gpu_layers=data['n_gpu_layers']
# chat_format="chatml"
# model_alias=os.path.basename(model)
# # 多模态
# clip_model_path=None
# prefix = "llava-phi-3-mini"
# file_name = prefix+"-mmproj-"
# if model_alias.startswith(prefix):
# for file in os.listdir(os.path.dirname(model)):
# 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)
# address="127.0.0.1"
# port=9090
# success = False
# for i in range(11): # 尝试最多11次
# if await check_port_available(address, port + i):
# port = port + i
# success = True
# break
# if success == False:
# return {"port":None,"model":""}
# 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
# )])
# 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.start()
# llama_port=port
# llama_model=data['model']
# llama_chat_format=chat_format
# return {"port":llama_port,"model":llama_model,"chat_format":llama_chat_format}
# llam服务的开启
# @routes.post('/mixlab/start_llama')
# async def my_hander_method(request):
# data =await request.json()
# # print(data)
# if llama_port and llama_model and llama_chat_format:
# return web.json_response({"port":llama_port,"model":llama_model,"chat_format":llama_chat_format} )
# try:
# result=await start_local_llm(data)
# except:
# result= {"port":None,"model":"","llama_cpp_error":True}
# print('start_local_llm error')
# return web.json_response(result)
# 重启服务
@routes.post('/mixlab/re_start')
def re_start(request):
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 94 KiB

+1 -1
View File
@@ -5,7 +5,7 @@ import torch
import folder_paths
from transformers import AutoTokenizer, AutoModel
from torchvision.transforms.v2 import ToPILImage
from decord import VideoReader, cpu # pip install decord
# from decord import VideoReader, cpu # pip install decord
from PIL import Image
def get_model_path(n=""):
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-mixlab-nodes"
description = "3D, ScreenShareNode & FloatingVideoNode, SpeechRecognition & SpeechSynthesis, GPT, LoadImagesFromLocal, Layers, Other Nodes, ..."
version = "0.39.0"
version = "0.40.0"
license = "MIT"
dependencies = ["numpy", "pyOpenSSL", "watchdog", "opencv-python-headless", "matplotlib", "openai", "simple-lama-inpainting", "clip-interrogator==0.6.0", "transformers>=4.36.0", "lark-parser", "imageio-ffmpeg", "rembg[gpu]", "omegaconf==2.3.0", "Pillow>=9.5.0", "einops==0.7.0", "trimesh>=4.0.5", "huggingface-hub", "scikit-image"]
-1
View File
@@ -20,6 +20,5 @@ torchaudio
soundfile>=0.12.1
json-repair
decord
bitsandbytes
accelerate
+1 -9
View File
@@ -229,15 +229,7 @@ async function extractInputAndOutputData (
return { input, output, seed, seedTitle }
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
async function save_app (json) {
let url = getUrl()
+55 -20
View File
@@ -93,28 +93,48 @@ async function* completion (url, messages, controller) {
return content
// return (await response.json()).content
}
export async function completion_ (apiKey, url, messages, controller, callback) {
let request = await chatCompletion(apiKey, url, messages, controller)
export async function completion_ (
apiKey,
url,
model_name,
messages,
controller,
callback
) {
let request = await chatCompletion(
apiKey,
url,
model_name,
messages,
controller
)
for await (const chunk of request) {
if (callback) callback(chunk)
}
}
export async function* chatCompletion (apiKey, url, messages, controller) {
url = `${getUrl()}/chat/completions`
export async function* chatCompletion (
apiKey,
api_url,
model_name,
messages,
controller
) {
const mixlabAPI = `${getUrl()}/chat/completions`
const requestBody = {
model: '01-ai/Yi-1.5-9B-Chat-16K',
messages: messages,
stream: true,
key: apiKey
key: apiKey,
model_name: model_name,
api_url
}
let response = await fetch(url, {
let response = await fetch(mixlabAPI, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`
'Content-Type': 'application/json'
// Authorization: `Bearer ${apiKey}`
},
body: JSON.stringify(requestBody),
mode: 'cors', // This is to ensure the request is made with CORS
@@ -134,15 +154,12 @@ export async function* chatCompletion (apiKey, url, messages, controller) {
if (result.done) {
break
}
// Add any leftover data to the current chunk of data
const text = leftover + decoder.decode(result.value)
// Check if the last character is a line break
const endsWithLineBreak = text.endsWith('\r\n')
const endsWithLineBreak = text.endsWith('\n')
// Split the text into lines
let lines = text.split('\r\n')
let lines = text.split('\n')
// If the text doesn't end with a line break, then the last line is incomplete
// Store it in leftover to be added to the next chunk of data
@@ -152,13 +169,31 @@ export async function* chatCompletion (apiKey, url, messages, controller) {
leftover = '' // Reset leftover if we have a line break at the end
}
// Parse all sse events and add them to result
const regex = /^(\S+):\s(.*)$/gm
for (const line of lines) {
if (line) {
content += line
yield line // Yield the trimmed line
} else {
cont = false
break
const match = regex.exec(line)
if (match) {
result[match[1]] = match[2]
// since we know this is llama.cpp, let's just decode the json in data
if (result.data) {
result.data = JSON.parse(result.data)
content += result.data.choices[0].delta?.content || ''
// console.log('#result.content',content)
// yield
yield result
// if we got a stop token from server, we will break here
if (result.data.choices[0].finish_reason == 'stop') {
if (result.data.generation_settings) {
// generation_settings = result.data.generation_settings;
}
cont = false
break
}
}
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.39.0'
const version = 'v0.40.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+53 -1
View File
@@ -8,6 +8,19 @@ export function getUrl () {
return url
}
// 获得插件/节点的索引数据
export async function get_nodes_map () {
let url = getUrl()
const res = await fetch(`${url}/mixlab/nodes_map`, {
method: 'POST',
body: JSON.stringify({
data: 'json'
})
})
return await res.json()
}
// 更新或者获取key
export const updateLLMAPIKey = async key => {
try {
@@ -74,7 +87,7 @@ export function get_position_style (
.scaleSelf(scaleX, scaleY)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
@@ -98,6 +111,32 @@ export function get_position_style (
}
}
export function loadCSS (url) {
var link = document.createElement('link')
link.rel = 'stylesheet'
link.type = 'text/css'
link.href = url
document.getElementsByTagName('head')[0].appendChild(link)
}
export function injectCSS (css) {
// 检查页面中是否已经存在具有相同内容的style标签
const existingStyle = document.querySelector('style')
if (existingStyle && existingStyle.textContent === css) {
return // 如果已经存在相同的样式,则不进行注入
}
// 创建一个新的style标签,并将CSS内容注入其中
const style = document.createElement('style')
style.textContent = css
// 将style标签插入到页面的head元素中
const head = document.querySelector('head')
head.appendChild(style)
}
export function loadExternalScript (url, type) {
return new Promise((resolve, reject) => {
const existingScript = document.querySelector(`script[src="${url}"]`)
@@ -158,6 +197,19 @@ export function createImage (url) {
})
}
export function convertImageUrlToBase64 (imageUrl) {
return fetch(imageUrl)
.then(response => response.blob())
.then(blob => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(blob)
})
})
}
export const getLocalData = key => {
let data = {}
try {
+26 -40
View File
@@ -3,51 +3,37 @@ import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
import { loadCSS, injectCSS } from './common.js'
import PhotoSwipeLightbox from '/mixlab/app/lib/photoswipe-lightbox.esm.min.js'
function loadCSS (url) {
var link = document.createElement('link')
link.rel = 'stylesheet'
link.type = 'text/css'
link.href = url
document.getElementsByTagName('head')[0].appendChild(link)
// Create a style element
const style = document.createElement('style')
// Define the CSS rule for scrollbar width
const cssRule = `.pswp__custom-caption {
background: rgb(20 27 70);
font-size: 16px;
color: #fff;
width: calc(100% - 32px);
max-width: 980px;
padding: 2px 8px;
border-radius: 4px;
position: absolute;
left: 50%;
bottom: 16px;
transform: translateX(-50%);
}
.pswp__custom-caption a {
color: #fff;
text-decoration: underline;
}
.hidden-caption-content {
display: none;
}`
// Add the CSS rule to the style element
style.appendChild(document.createTextNode(cssRule))
// Append the style element to the document head
document.head.appendChild(style)
}
loadCSS('/mixlab/app/lib/photoswipe.min.css')
injectCSS(`.pswp__custom-caption {
background: rgb(20 27 70);
font-size: 16px;
color: #fff;
width: calc(100% - 32px);
max-width: 980px;
padding: 2px 8px;
border-radius: 4px;
position: absolute;
left: 50%;
bottom: 16px;
transform: translateX(-50%);
}
.pswp__custom-caption a {
color: #fff;
text-decoration: underline;
}
.hidden-caption-content {
display: none;
}`)
function initLightBox () {
const lightbox = new PhotoSwipeLightbox({
gallery: '.prompt_image_output',
children: 'a',
pswpModule: () =>
import('/mixlab/app/lib/photoswipe.esm.min.js')
pswpModule: () => import('/mixlab/app/lib/photoswipe.esm.min.js')
})
lightbox.on('uiRegister', function () {
@@ -101,9 +87,9 @@ function get_position_style (ctx, widget_width, y, node_height) {
transformOrigin: '0 0',
transform: transform,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
+213 -381
View File
@@ -11,9 +11,31 @@ import { smart_init, addSmartMenu } from './smart_connect.js'
import { completion_ } from './chat.js'
import { getLocalData, saveLocalData, updateLLMAPIKey } from './common.js'
import {
getLocalData,
saveLocalData,
updateLLMAPIKey,
convertImageUrlToBase64,
get_nodes_map,
injectCSS,
loadCSS,
loadExternalScript
} from './common.js'
injectCSS(`
.help_link {
background: linear-gradient(rgb(110 110 110 / 50%), rgba(255, 255, 0, 0));
background-size: 200% 200%;
transition: background-position 0.5s;
text-decoration: none;
}
.help_link:hover {
background-position: right bottom;
}`)
const BIZYAIR_SERVER_ADDRESS = 'https://api.siliconflow.cn'
const BIZYAIR_MODEL = '01-ai/Yi-1.5-9B-Chat-16K'
function showTextByLanguage (key, json) {
// 获取浏览器语言
@@ -32,35 +54,6 @@ function showTextByLanguage (key, json) {
//系统prompt
// const systemPrompt = `You are a prompt creator, your task is to create prompts for the user input request, the prompts are image descriptions that include keywords for (an adjective, type of image, framing/composition, subject, subject appearance/action, environment, lighting situation, details of the shoot/illustration, visuals aesthetics and artists), brake keywords by comas, provide high quality, non-verboose, coherent, brief, concise, and not superfluous prompts, the subject from the input request must be included verbatim on the prompt,the prompt is english`
let tool = {
name: 'create_prompt',
description:
'Create a prompt with a given subject, content, and style based on user input for image descriptions.',
parameter: {
type: 'object',
properties: {
subject: {
type: 'string',
description:
'The subject of the prompt, included verbatim from the input request.',
required: true
},
content: {
type: 'string',
description:
'The content of the prompt, primarily focusing on the scene and objects, including keywords for adjective, type of image, framing/composition, subject appearance/action, and environment.',
required: true
},
style: {
type: 'string',
description:
'The style of the prompt, including lighting situation, details of the shoot/illustration, visual aesthetics, and artists. Ensure it is high quality, non-verbose, coherent, brief, concise, and not superfluous.',
required: true
}
}
}
}
const systemPrompt = `
Prompt:
@@ -215,34 +208,6 @@ async function createMenu () {
let isScriptLoaded = {}
function loadExternalScript (url) {
return new Promise((resolve, reject) => {
if (isScriptLoaded[url]) {
resolve()
return
}
const existingScript = document.querySelector(`script[src="${url}"]`)
if (existingScript) {
existingScript.onload = () => {
isScriptLoaded[url] = true
resolve()
}
existingScript.onerror = reject
return
}
const script = document.createElement('script')
script.src = url
script.onload = () => {
isScriptLoaded[url] = true
resolve()
}
script.onerror = reject
document.head.appendChild(script)
})
}
//
function createChart (chartDom, nodes) {
@@ -471,20 +436,6 @@ function deepEqual (obj1, obj2) {
return true
}
async function get_nodes_map () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
const res = await fetch(`${url}/mixlab/nodes_map`, {
method: 'POST',
body: JSON.stringify({
data: 'json'
})
})
return await res.json()
}
function get_url () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
@@ -525,34 +476,10 @@ async function get_my_app (filename = null, category = '') {
return data
}
function loadCSS (url) {
var link = document.createElement('link')
link.rel = 'stylesheet'
link.type = 'text/css'
link.href = url
document.getElementsByTagName('head')[0].appendChild(link)
}
var cssURL =
'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-markdown-light.min.css'
loadCSS(cssURL)
function injectCSS (css) {
// 检查页面中是否已经存在具有相同内容的style标签
const existingStyle = document.querySelector('style')
if (existingStyle && existingStyle.textContent === css) {
return // 如果已经存在相同的样式,则不进行注入
}
// 创建一个新的style标签,并将CSS内容注入其中
const style = document.createElement('style')
style.textContent = css
// 将style标签插入到页面的head元素中
const head = document.querySelector('head')
head.appendChild(style)
}
injectCSS(`::-webkit-scrollbar {
width: 2px;
}
@@ -611,46 +538,20 @@ injectCSS(`::-webkit-scrollbar {
`)
async function getCustomnodeMappings (mode = 'url') {
// mode = "local";
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
async function getCustomnodeMappings () {
let nodes = {}
const data = (await get_nodes_map()).data
for (let url in data) {
let n = data[url]
if (!window._nodes_maps) {
const data = (await get_nodes_map()).data
window._nodes_maps = data
}
console.log('#getCustomnodeMappings', window._nodes_maps)
for (let url in window._nodes_maps) {
let n = window._nodes_maps[url]
for (let node of n[0]) {
// if(node=='CLIPSeg')console.log('#CLIPSeg',n)
nodes[node] = { url, title: n[1].title_aux }
}
}
// try {
// const response = await fetch(`${url}/customnode/getmappings?mode=${mode}`)
// const data = await response.json()
// for (let url in data) {
// let n = data[url]
// for (let node of n[0]) {
// // if(node=='CLIPSeg')console.log('#CLIPSeg',n)
// nodes[node] = { url, title: n[1].title_aux }
// }
// }
// } catch (error) {
// const data = (await get_nodes_map()).data
// for (let url in data) {
// let n = data[url]
// for (let node of n[0]) {
// // if(node=='CLIPSeg')console.log('#CLIPSeg',n)
// nodes[node] = { url, title: n[1].title_aux }
// }
// }
// }
return nodes
}
@@ -661,10 +562,17 @@ const missingNodeGithub = (missingNodeTypes, nodesMap) => {
if (nodesMap[n]) {
let title = nodesMap[n].title
if (!ts[title]) {
const link = nodesMap[n].url
// 判断链接是否为GitHub仓库链接
const githubRegex = /^https:\/\/github\.com\/(?:.*?\/)?([^/]+)\/.+$/
const author = link.match(githubRegex)[1]
console.log(`(作者: ${author})`)
ts[title] = {
title,
nodes: {},
url: nodesMap[n].url
url: link,
author
}
}
ts[title].nodes[n] = 1
@@ -680,50 +588,45 @@ const missingNodeGithub = (missingNodeTypes, nodesMap) => {
return Array.from(Object.values(ts), n => {
const url = n.url
return `<li style="color: white;
background: black;
return `<a
href="${url}"
target="_blank"
title="${url}"
style="color: white;
padding: 8px;
font-size: 12px;">${n.title}<a href="${url}" target="_blank"> 🔗</a></li>`
font-size: 16px;
display: flex;
flex-direction:${!n.author ? 'row' : 'column'};
"
class="help_link"
>${n.title}
<div
style="display: flex;
flex-direction: row;
align-items: center;
${!n.author ? 'line-height: 4px;' : ''}
"
>
${
n.author
? `
<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" alt="GitHub Logo" width="24" height="24"/>
<p style="line-height: 14px;
color: white;
margin-left: 12px;
}">Author:${n.author}</p>
`
: '🔍'
}
</div></a>`
})
}
let nodesMap
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:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `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'
}
}
// Enhanced navigation to GitHub for missing node search upon graph load.
// 更好地错误提示,找到GitHub原仓库地址
app.showMissingNodesError = async function (
missingNodeTypes,
hasAddedNodes = true
@@ -731,24 +634,10 @@ app.showMissingNodesError = async function (
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
: await getCustomnodeMappings()
// console.log('#nodesMap', nodesMap)
// console.log('###MIXLAB', missingNodeTypes, hasAddedNodes)
this.ui.dialog.show(
`<a style="color: white;
font-size: 18px;
font-weight: 800;
letter-spacing: 2px;
font-family: sans-serif;
}"
href="https://discord.gg/cXs9vZSqeK" target="_blank">${showTextByLanguage(
'Welcome to Mixlab nodes discord, seeking help.',
{
'Welcome to Mixlab nodes discord, seeking help.':
'寻求帮助,加入Mixlab nodes交流频道'
}
)}</a><br><br>${showTextByLanguage(
`${showTextByLanguage(
'When loading the graph, the following node types were not found:',
{
'When loading the graph, the following node types were not found:':
@@ -756,73 +645,34 @@ app.showMissingNodesError = async function (
}
)}
<ul>${missingNodeGithub(missingNodeTypes, nodesMap).join('')}</ul>${
hasAddedNodes ? '' : ''
}`
<ul class="comfy-missing-nodes">${missingNodeGithub(
missingNodeTypes,
nodesMap
).join('')}</ul>${hasAddedNodes ? '' : ''}
<br><br><a
style="color: #dedede;
font-size: 16px;
font-weight: 600;
letter-spacing: 2px;
font-family: sans-serif;
text-decoration: none;
"
class="help_link"
href="https://discord.gg/cXs9vZSqeK" target="_blank">${showTextByLanguage(
'Welcome to Mixlab nodes discord, seeking help.',
{
'Welcome to Mixlab nodes discord, seeking help.':
'寻求帮助,加入Mixlab nodes交流频道'
}
)}</a>
`
)
this.logging.addEntry('Comfy.App', 'warn', {
MissingNodes: missingNodeTypes
})
}
// app.registerExtension({
// name: 'Comfy.MDNote',
// registerCustomNodes () {
// class NoteNode {
// // color = LGraphCanvas.node_colors.yellow.color
// // bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
// // groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
// constructor () {
// if (!this.properties) {
// this.properties = {}
// this.properties.text = ''
// }
// console.log('NoteNode1', this)
// const widget = {
// type: 'div',
// name: 'input_color',
// draw (ctx, node, widget_width, y, widget_height) {
// Object.assign(
// this.div.style,
// get_position_style(
// ctx,
// widget_width,
// 44,
// node.size[1]
// )
// )
// }
// }
// widget.div = $el('div', {});
// widget.div.innerText='1111'
// document.body.appendChild(widget.div)
// this.addCustomWidget(widget)
// this.serialize_widgets = true
// this.isVirtualNode = true
// }
// }
// // Load default visibility
// LiteGraph.registerNodeType(
// 'MDNote',
// Object.assign(NoteNode, {
// title_mode: LiteGraph.NORMAL_TITLE,
// title: 'MDNote',
// collapsable: true
// })
// )
// NoteNode.category = '♾️Mixlab/utils'
// },
// })
// 读取仓库说明
async function fetchReadmeContent (url) {
try {
// var repo = 'owner/repo'; // 仓库的拥有者和名称
@@ -843,29 +693,6 @@ async function fetchReadmeContent (url) {
}
}
async function startLLM (model) {
let res = await start_llama(model)
window._mixlab_llamacpp = res || { model: [] }
localStorage.setItem('_mixlab_llama_select', res?.model || '')
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)
}
if (
document.body.querySelector('#llm_status_btn') &&
window._mixlab_llamacpp
) {
document.body.querySelector('#llm_status_btn').innerText =
window._mixlab_llamacpp.model
}
}
function createInputOfLabel (labelText, key, id) {
const label = document.createElement('p')
label.innerText = labelText
@@ -1004,18 +831,21 @@ function createModelsModal (models, llmKey) {
align-items: center;
font-size: 12px;`
batchPageBtn.innerHTML = `<a href="${get_url()}/mixlab/app" target="_blank" style="color: var(--input-text);
background-color: var(--comfy-input-bg);">App</a>`
background-color: var(--comfy-input-bg);font-size: 16px;">MixLab App</a>`
const siliconflowHelp = document.createElement('a')
siliconflowHelp.textContent = showTextByLanguage('Siliconflow', {
Siliconflow: '硅基流动'
})
siliconflowHelp.textContent =
showTextByLanguage('Use Siliconflow', {
'Use Siliconflow': '使用硅基流动'
}) +
'\n' +
showTextByLanguage('Or Local LLM', {
'Or Local LLM': '或者本地LLM'
})
siliconflowHelp.style = `color: var(--input-text);
background-color: var(--comfy-input-bg);margin-top:14px`
background-color: var(--comfy-input-bg);margin-top:14px;font-size: 16px;`
siliconflowHelp.href = 'https://cloud.siliconflow.cn/s/mixlabs'
siliconflowHelp.target = '_blank'
siliconflowHelp.target = '_blank'
const title = document.createElement('p')
title.innerText = 'Mixlab Nodes'
@@ -1082,11 +912,25 @@ function createModelsModal (models, llmKey) {
let llmKeyDiv = createInputOfLabel('LLM Key', '_mixlab_llm_api_key', '-')
saveLocalData('_mixlab_llm_api_url', '-', BIZYAIR_SERVER_ADDRESS)
if (!getLocalData('_mixlab_llm_api_url')['-']) {
saveLocalData('_mixlab_llm_api_url', '-', BIZYAIR_SERVER_ADDRESS)
}
let llmAPIDiv = createInputOfLabel('LLM API', '_mixlab_llm_api_url', '-')
if (!getLocalData('_mixlab_llm_model_name')['-']) {
saveLocalData('_mixlab_llm_model_name', '-', BIZYAIR_MODEL)
}
let llmModelDiv = createInputOfLabel(
'LLM Model',
'_mixlab_llm_model_name',
'-'
)
modalContent.appendChild(llmKeyDiv)
modalContent.appendChild(llmAPIDiv)
modalContent.appendChild(llmModelDiv)
var inputForSystemPrompt = document.createElement('textarea')
inputForSystemPrompt.className = 'comfy-multiline-input'
@@ -1433,19 +1277,6 @@ function drawBadge (node, orig, restArgs) {
return r
}
function convertImageUrlToBase64 (imageUrl) {
return fetch(imageUrl)
.then(response => response.blob())
.then(blob => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(blob)
})
})
}
async function getSelectImageNode () {
var nodes = app.canvas.selected_nodes
let imageNode = null
@@ -1463,23 +1294,23 @@ app.registerExtension({
name: 'Comfy.Mixlab.ui',
init () {
//是否要自动加载模型
if (localStorage.getItem('_mixlab_auto_llama_open')) {
let model = localStorage.getItem('_mixlab_llama_select')
start_llama(model).then(res => {
window._mixlab_llamacpp = res
document.body
.querySelector('#mixlab_chatbot_by_llamacpp')
.setAttribute('title', res.url)
})
} else {
// startLLM('')
}
// if (localStorage.getItem('_mixlab_auto_llama_open')) {
// let model = localStorage.getItem('_mixlab_llama_select')
// start_llama(model).then(res => {
// window._mixlab_llamacpp = res
// document.body
// .querySelector('#mixlab_chatbot_by_llamacpp')
// .setAttribute('title', res.url)
// })
// } else {
// // startLLM('')
// }
LGraphCanvas.prototype.helpAboutNode = async function (node) {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
: await getCustomnodeMappings()
console.log(
'%c### node & node map',
@@ -1491,6 +1322,7 @@ app.registerExtension({
let repo = nodesMap[node.type]
if (repo) {
let markdown = await fetchReadmeContent(repo.url)
await loadExternalScript('/mixlab/app/lib/showdown.min.js')
createModal(repo.url, markdown, repo.title)
}
}
@@ -1527,7 +1359,8 @@ app.registerExtension({
Object.values(getLocalData('_mixlab_llm_api_key'))[0],
getLocalData('_mixlab_llm_api_url')['-'] ||
Object.values(getLocalData('_mixlab_llm_api_url'))[0],
getLocalData('_mixlab_llm_model_name')['-'] ||
Object.values(getLocalData('_mixlab_llm_model_name'))[0],
[
{
role: 'system',
@@ -1537,15 +1370,16 @@ app.registerExtension({
],
controller,
t => {
// console.log(t.endsWith('\r'))
widget.value += t
jsonStr += t
let content = t.data?.choices[0]?.delta?.content || ''
console.log(content)
widget.value += content
// jsonStr += content
}
)
} catch (error) {
console.log(error)
}
}
}
@@ -1573,79 +1407,76 @@ app.registerExtension({
widget.value += '\n'
try {
await completion_(
window._mixlab_llamacpp.url + '/v1/chat/completions',
[
{
role: 'system',
content: localStorage.getItem('_mixlab_system_prompt')
},
// { role: 'user', content: userInput }
{
role: 'user',
content: [
{
type: 'image_url',
image_url: {
url: imageBase64
}
},
{ type: 'text', text: 'What’s in this image?' }
]
}
],
controller,
t => {
// console.log(t)
widget.value += t
NoteNode.size[1] = widget.element.scrollHeight + 20
widget.computedHeight = NoteNode.size[1]
app.canvas.centerOnNode(NoteNode)
}
)
// await completion_(
// window._mixlab_llamacpp.url + '/v1/chat/completions',
// [
// {
// role: 'system',
// content: localStorage.getItem('_mixlab_system_prompt')
// },
// // { role: 'user', content: userInput }
// {
// role: 'user',
// content: [
// {
// type: 'image_url',
// image_url: {
// url: imageBase64
// }
// },
// { type: 'text', text: 'What’s in this image?' }
// ]
// }
// ],
// controller,
// t => {
// // console.log(t)
// widget.value += t
// NoteNode.size[1] = widget.element.scrollHeight + 20
// widget.computedHeight = NoteNode.size[1]
// app.canvas.centerOnNode(NoteNode)
// }
// )
} catch (error) {
//是否要自动加载模型
if (localStorage.getItem('_mixlab_auto_llama_open')) {
let model = localStorage.getItem('_mixlab_llama_select')
start_llama(model).then(async res => {
window._mixlab_llamacpp = res
document.body
.querySelector('#mixlab_chatbot_by_llamacpp')
.setAttribute('title', res.url)
await completion_(
window._mixlab_llamacpp.url + '/v1/chat/completions',
[
{
role: 'system',
content: localStorage.getItem('_mixlab_system_prompt')
},
{
role: 'user',
content: [
{
type: 'image_url',
image_url: {
url: imageBase64
}
},
{ type: 'text', text: 'What’s in this image?' }
]
}
],
controller,
t => {
// console.log(t)
widget.value += t
NoteNode.size[1] = widget.element.scrollHeight + 20
widget.computedHeight = NoteNode.size[1]
app.canvas.centerOnNode(NoteNode)
}
)
})
}
// if (localStorage.getItem('_mixlab_auto_llama_open')) {
// let model = localStorage.getItem('_mixlab_llama_select')
// start_llama(model).then(async res => {
// window._mixlab_llamacpp = res
// document.body
// .querySelector('#mixlab_chatbot_by_llamacpp')
// .setAttribute('title', res.url)
// await completion_(
// window._mixlab_llamacpp.url + '/v1/chat/completions',
// [
// {
// role: 'system',
// content: localStorage.getItem('_mixlab_system_prompt')
// },
// {
// role: 'user',
// content: [
// {
// type: 'image_url',
// image_url: {
// url: imageBase64
// }
// },
// { type: 'text', text: 'What’s in this image?' }
// ]
// }
// ],
// controller,
// t => {
// // console.log(t)
// widget.value += t
// NoteNode.size[1] = widget.element.scrollHeight + 20
// widget.computedHeight = NoteNode.size[1]
// app.canvas.centerOnNode(NoteNode)
// }
// )
// })
// }
}
widget.value = widget.value.trim()
@@ -1839,7 +1670,8 @@ app.registerExtension({
if (
text_widget &&
text_widget.length == 1 &&
llm_api_key &&llm_api_url&&
llm_api_key &&
llm_api_url &&
node.type != 'ShowTextForGPT'
) {
opts.push({
@@ -2058,7 +1890,7 @@ app.registerExtension({
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
: await getCustomnodeMappings()
const nodesDiv = document.createDocumentFragment()
const nodes = (await app.graphToPrompt()).output
+5 -22
View File
@@ -3,24 +3,7 @@ import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
// The code is based on ComfyUI-VideoHelperSuite modification.
function injectCSS (css) {
// 检查页面中是否已经存在具有相同内容的style标签
const existingStyle = document.querySelector('style')
if (existingStyle && existingStyle.textContent === css) {
return // 如果已经存在相同的样式,则不进行注入
}
// 创建一个新的style标签,并将CSS内容注入其中
const style = document.createElement('style')
style.textContent = css
// 将style标签插入到页面的head元素中
const head = document.querySelector('head')
head.appendChild(style)
}
import { injectCSS } from './common.js'
injectCSS(`
.hidden{
@@ -44,9 +27,9 @@ function get_position_style (ctx, widget_width, y, node_height) {
transformOrigin: '0 0',
transform: transform,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
@@ -471,7 +454,7 @@ app.registerExtension({
const prefix = 'vhs_gif_preview_'
const r = onExecuted ? onExecuted.apply(this, message) : undefined
if(!this.widgets) this.widgets=[]
if (!this.widgets) this.widgets = []
if (this.widgets) {
const pos = this.widgets.findIndex(w => w.name === `${prefix}_0`)