diff --git a/__init__.py b/__init__.py
index 94bc4dd..1f6b527 100644
--- a/__init__.py
+++ b/__init__.py
@@ -11,6 +11,10 @@ import logging
from comfy.cli_args import args
python = sys.executable
+
+llama_port=None
+llama_model=""
+
from .nodes.ChatGPT import get_llama_models,get_llama_model_path
from server import PromptServer
@@ -624,11 +628,13 @@ async def post_prompt_result(request):
# llam服务的开启
-
-
@routes.post('/mixlab/start_llama')
async def my_hander_method(request):
data = await request.json()
+ global llama_port,llama_model
+
+ if llama_port and llama_model:
+ return web.json_response({"port":llama_port,"model":llama_model})
import threading
import uvicorn
@@ -640,12 +646,25 @@ async def my_hander_method(request):
ConfigFileSettings,
)
model=get_llama_model_path(data['model'])
+
+ address="127.0.0.1"
port=9090
- server_settings=ServerSettings(host="127.0.0.1",port=port)
+ 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 web.json_response({"port":None,"model":""})
+
+
+ server_settings=ServerSettings(host=address,port=port)
app = create_app(
server_settings=server_settings,
- model_settings=[ModelSettings(model=model)],
+ model_settings=[ModelSettings(model=model,n_gpu_layers=9999,n_ctx=4098)],
)
def run_uvicorn():
@@ -663,7 +682,10 @@ async def my_hander_method(request):
# 启动子线程
thread.start()
- return web.json_response({"port":port,"model":model})
+ llama_port=port
+ llama_model=data['model']
+
+ return web.json_response({"port":llama_port,"model":llama_model})
diff --git a/web/javascript/chat.js b/web/javascript/chat.js
new file mode 100644
index 0000000..75a0db5
--- /dev/null
+++ b/web/javascript/chat.js
@@ -0,0 +1,108 @@
+async function* completion (url, messages, controller) {
+ let data = {
+ model: 'gpt-3.5-turbo-16k',
+ messages,
+ temperature: 0.6,
+ stream: true
+ }
+ // if (imageNode) {
+ // data = { ...data, image_data: [imageNode] }
+ // }
+
+ // let controller = new AbortController()
+
+ let response = await fetch(url, {
+ method: 'POST',
+ body: JSON.stringify(data),
+ headers: {
+ Connection: 'keep-alive',
+ 'Content-Type': 'application/json',
+ Accept: 'text/event-stream'
+ },
+ signal: controller.signal
+ })
+
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder()
+
+ let content = ''
+ let leftover = '' // Buffer for partially read lines
+
+ try {
+ let cont = true
+ while (cont) {
+ let result = await reader.read()
+ 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('\n')
+
+ // Split the text into lines
+ 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
+ if (!endsWithLineBreak) {
+ leftover = lines.pop()
+ } else {
+ 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) {
+ 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)
+ // console.log('#result.data',result.data)
+
+ content += result.data.choices[0].delta?.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
+ }
+ }
+ }
+ }
+ }
+ } catch (e) {
+ console.error('llama error: ', e)
+ throw e
+ } finally {
+ controller.abort()
+ }
+
+ return content
+ // return (await response.json()).content
+}
+
+export async function completion_ (url, messages, controller, callback) {
+ let request = await completion(url, messages, controller)
+ for await (const chunk of request) {
+
+ let content=chunk.data.choices[0].delta.content||""
+ if(chunk.data.choices[0].role=="assistant"){
+ //开始
+ content=""
+ }
+
+ if (callback) callback(content)
+ }
+}
diff --git a/web/javascript/ui_mixlab.js b/web/javascript/ui_mixlab.js
index b844501..e325ebd 100644
--- a/web/javascript/ui_mixlab.js
+++ b/web/javascript/ui_mixlab.js
@@ -9,6 +9,15 @@ import {
import { smart_init, addSmartMenu } from './smart_connect.js'
+import { completion_ } from './chat.js'
+
+//系统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`
+
+if (!localStorage.getItem('_mixlab_system_prompt')) {
+ localStorage.setItem('_mixlab_system_prompt', systemPrompt)
+}
+
// 获取llama 模型
async function get_llamafile_models () {
try {
@@ -43,13 +52,45 @@ async function start_llama (model = 'Phi-3-mini-4k-instruct-Q5_K_S.gguf') {
})
const data = await response.json()
- return `http://127.0.0.1:${data.port}`
+ return { url: `http://127.0.0.1:${data.port}`, model: data.model }
} catch (error) {
console.error(error)
}
}
+// 菜单入口
+async function createMenu () {
+ const menu = document.querySelector('.comfy-menu')
+ const separator = document.createElement('div')
+ separator.style = `margin: 20px 0px;
+ width: 100%;
+ height: 1px;
+ background: var(--border-color);
+ `
+ menu.append(separator)
+ if (!menu.querySelector('#mixlab_chatbot_by_llamacpp')) {
+ const appsButton = document.createElement('button')
+ appsButton.id = 'mixlab_chatbot_by_llamacpp'
+ appsButton.textContent = 'llamacpp♾️Mixlab'
+
+ // appsButton.onclick = () =>
+ appsButton.onclick = async () => {
+ if (window._mixlab_llamacpp) {
+ //显示运行的模型
+ createModelsModal([
+ window._mixlab_llamacpp.url,
+ window._mixlab_llamacpp.model
+ ])
+ } else {
+ let ms = await get_llamafile_models()
+ ms = ms.filter(m => !m.match('-mmproj-'))
+ if (ms.length > 0) createModelsModal(ms)
+ }
+ }
+ menu.append(appsButton)
+ }
+}
let isScriptLoaded = {}
@@ -387,6 +428,33 @@ injectCSS(`::-webkit-scrollbar {
width: 2px;
}
+#mixlab_chatbot_by_llamacpp{
+ font-size:14px
+}
+
+#mixlab_chatbot_by_llamacpp::before {
+ content: attr(title);
+ position: absolute;
+ margin-top: 24px;
+ font-size: 10px;
+}
+
+.mix_tag{
+ padding:8px;cursor: pointer;font-size: 14px;
+ color: var(--input-text);
+ background-color: var(--comfy-input-bg);
+ border-radius: 8px;
+ border-color: var(--border-color);
+ border-style: solid;
+ margin-top: 2px;
+ margin-bottom: 14px;
+}
+
+.mix_tag:hover{
+ background-color: #101c19;
+ color: aquamarine;
+}
+
@keyframes loading_mixlab {
0% {
background-color: green;
@@ -629,13 +697,160 @@ async function fetchReadmeContent (url) {
}
}
+function createModelsModal (models) {
+ var div =
+ document.querySelector('#model-modal') || document.createElement('div')
+ div.id = 'model-modal'
+ div.innerHTML = ''
+ div.style.cssText = `
+ width: 100%;
+ z-index: 9990;
+ height: 100vh;
+ display: flex;
+ color: var(--descrip-text);
+ position: fixed;
+ top: 0;
+ left: 0;
+ background: #000000a8;
+ `
+
+ var modal = document.createElement('div')
+
+ div.addEventListener('click', e => {
+ e.stopPropagation()
+ div.remove()
+ })
+
+ div.appendChild(modal)
+ modal.classList.add('modal-body')
+ // Set modal styles
+ modal.style.cssText = `
+ color: var(--descrip-text);
+ background-color: var(--comfy-menu-bg);
+ position: fixed;
+ overflow:hidden;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 9999;
+ border-radius: 4px;
+ box-shadow: 4px 4px 14px rgba(255,255,255,0.2);
+ `
+
+ // Create modal header
+ const headerElement = document.createElement('div')
+ headerElement.classList.add('modal-header')
+ headerElement.style.cssText = `
+ display: flex;
+ padding: 20px 24px 8px 24px;
+ justify-content: space-between;
+ `
+
+ const headTitleElement = document.createElement('a')
+ headTitleElement.classList.add('header-title')
+ headTitleElement.style.cssText = `
+ color: var(--descrip-text);
+ font-size: 18px;
+ display: flex;
+ align-items: center;
+ flex: 1;
+ overflow: hidden;
+ text-decoration: none;
+ font-weight: bold;
+ justify-content: space-between;
+ padding: 20px;
+ cursor: pointer;
+ user-select: none;
+ `
+
+ headTitleElement.textContent = 'Models'
+ // headTitleElement.href = 'https://github.com/shadowcz007/comfyui-mixlab-nodes'
+ // headTitleElement.target = '_blank'
+ const linkIcon = document.createElement('small')
+ linkIcon.textContent = '自动开启'
+ linkIcon.style.padding = '4px'
+
+ headTitleElement.appendChild(linkIcon)
+ headerElement.appendChild(headTitleElement)
+ if (localStorage.getItem('_mixlab_auto_llama_open')) {
+ linkIcon.style.backgroundColor = '#66ff6c'
+ linkIcon.style.color = 'black'
+ }
+ linkIcon.addEventListener('click', e => {
+ e.stopPropagation()
+ if (localStorage.getItem('_mixlab_auto_llama_open')) {
+ localStorage.setItem('_mixlab_auto_llama_open', '')
+ linkIcon.style.backgroundColor = ''
+ linkIcon.style.color = 'var(--descrip-text)'
+ } else {
+ localStorage.setItem('_mixlab_auto_llama_open', 'true')
+ linkIcon.style.backgroundColor = '#66ff6c'
+ linkIcon.style.color = 'black'
+ }
+ })
+
+ modal.appendChild(headTitleElement)
+
+ // Create modal content area
+ var modalContent = document.createElement('div')
+ modalContent.classList.add('modal-content')
+
+ var input = document.createElement('textarea')
+ input.className = 'comfy-multiline-input'
+ input.style = ` height: 260px;
+ width: 480px;
+ font-size: 16px;
+ padding: 18px;`
+ input.value = localStorage.getItem('_mixlab_system_prompt')
+
+ input.addEventListener('change', e => {
+ e.stopPropagation()
+ localStorage.setItem('_mixlab_system_prompt', input.value)
+ })
+
+ input.addEventListener('click', e => {
+ e.stopPropagation()
+ })
+
+ modalContent.appendChild(input)
+
+ for (const m of models) {
+ let d = document.createElement('div')
+ d.innerText = m
+ d.className = `mix_tag`
+
+ if (!window._mixlab_llamacpp) {
+ d.addEventListener('click', async e => {
+ e.stopPropagation()
+ div.remove()
+ let res = await start_llama(m)
+ window._mixlab_llamacpp = res
+
+ localStorage.setItem('_mixlab_llama_select', res.model)
+
+ if (document.body.querySelector('#mixlab_chatbot_by_llamacpp')) {
+ document.body
+ .querySelector('#mixlab_chatbot_by_llamacpp')
+ .setAttribute('title', window._mixlab_llamacpp.url)
+ }
+ })
+ }
+
+ modalContent.appendChild(d)
+ }
+ modal.appendChild(modalContent)
+
+ document.body.appendChild(div)
+}
+
function createModal (url, markdown, title) {
// Create modal element
var div =
document.querySelector('#mix-modal') || document.createElement('div')
div.id = 'mix-modal'
div.innerHTML = ''
- div.style.cssText = `width: 100%;
+ div.style.cssText = `
+ width: 100%;
z-index: 9990;
height: 100vh;
display: flex;
@@ -941,6 +1156,17 @@ function drawBadge (node, orig, restArgs) {
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)
+ })
+ }
+
LGraphCanvas.prototype.helpAboutNode = async function (node) {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
@@ -965,12 +1191,57 @@ app.registerExtension({
smart_init()
+ LGraphCanvas.prototype.text2text = async function (node) {
+ // console.log(node)
+ let widget = node.widgets.filter(
+ w => w.name === 'text' && typeof w.value == 'string'
+ )[0]
+ if (widget) {
+ let controller = new AbortController()
+ let ends = []
+ let userInput = widget.value
+ widget.value = widget.value.trim()
+ widget.value += '\n'
+ await completion_(
+ window._mixlab_llamacpp.url + '/v1/chat/completions',
+ [
+ {
+ role: 'system',
+ content: localStorage.getItem('_mixlab_system_prompt')
+ },
+ { role: 'user', content: userInput }
+ ],
+ controller,
+ t => {
+ // console.log(t)
+ widget.value += t
+ }
+ )
+ widget.value = widget.value.trim()
+ // await chat(
+ // userInput,
+ // await getSelectImageNode(),
+ // t => {
+ // widget.value += t
+ // //有回车则终止
+ // t = t.replace(/\n/g, '
')
+ // ends.push(t.trim())
+ // if (hasRepeatingPhrases(ends.join(' '))) t = '
'
+ // if (t.trim() == '
') {
+ // controller.abort()
+ // }
+ // },
+ // controller
+ // )
+ }
+ }
+
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions // store the existing method
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
// replace it
const options = getNodeMenuOptions.apply(this, arguments) // start by calling the stored one
node.setDirtyCanvas(true, true) // force a redraw of (foreground, background)
- console.log('getNodeMenuOptions', node.type == 'CLIPTextEncode')
+ console.log('#getNodeMenuOptions', node.type)
let opts = [
{
@@ -988,19 +1259,39 @@ app.registerExtension({
]
if (node.widgets) {
- // let text_widget = node.widgets.filter(
- // w => w.name === 'text' && typeof w.value == 'string'
- // )
- // if (text_widget && text_widget.length == 1) {
- // opts.push({
- // content: 'Text-to-Text ♾️Mixlab', // with a name
- // callback: () => {
- // LGraphCanvas.prototype.text2text(node)
- // } // and the callback
- // })
- // }
+ let text_widget = node.widgets.filter(
+ w => w.name === 'text' && typeof w.value == 'string'
+ )
+
+ let text_input = node.inputs.filter(
+ inp => inp.name == 'text' && inp.type == 'STRING'
+ )
+
+ if (
+ text_input.length == 0 &&
+ text_widget &&
+ text_widget.length == 1 &&
+ window._mixlab_llamacpp &&
+ node.type != 'ShowTextForGPT'
+ ) {
+ opts.push({
+ content: 'Text-to-Text ♾️Mixlab', // with a name
+ callback: () => {
+ LGraphCanvas.prototype.text2text(node)
+ } // and the callback
+ })
+ }
}
+ // if (node.imgs && node.imgs.length > 0) {
+ // opts.push({
+ // content: 'Image-to-Text ♾️Mixlab', // with a name
+ // callback: () => {
+ // LGraphCanvas.prototype.text2text(node)
+ // } // and the callback
+ // })
+ // }
+
opts = addSmartMenu(opts, node)
// if (node.type == 'CLIPTextEncode') {
@@ -1182,6 +1473,56 @@ app.registerExtension({
this.setDirty(true, true)
}
+ LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
+ // replace it
+ const options = getNodeMenuOptions.apply(this, arguments) // start by calling the stored one
+ node.setDirtyCanvas(true, true) // force a redraw of (foreground, background)
+
+ let opts = []
+
+ if (node.widgets) {
+ let text_widget = node.widgets.filter(
+ w => w.name === 'text' && typeof w.value == 'string'
+ )
+
+ if (text_widget && text_widget.length == 1) {
+ opts = [
+ {
+ content: 'Text-to-Text ♾️Mixlab', // with a name
+ callback: () => {
+ LGraphCanvas.prototype.text2text(node)
+ } // and the callback
+ }
+ // {
+ // content: 'Fix node v2', // with a name
+ // callback: () => {
+ // LGraphCanvas.prototype.fixTheNode(node)
+ // }
+ // }
+ ]
+ }
+ }
+
+ if (node.imgs && node.imgs.length > 0) {
+ opts = [
+ {
+ content: 'Image-to-Text ♾️Mixlab', // with a name
+ callback: () => {
+ LGraphCanvas.prototype.text2text(node)
+ } // and the callback
+ }
+ // {
+ // content: 'Fix node v2', // with a name
+ // callback: () => {
+ // LGraphCanvas.prototype.fixTheNode(node)
+ // }
+ // }
+ ]
+ }
+
+ return [...opts, null, ...options] // and return the options
+ }
+
// 支持app模式的json
const loadAppJson = async data => {
let workflow
@@ -1229,6 +1570,8 @@ app.registerExtension({
}
})
}
+
+ createMenu()
},
setup () {
setTimeout(async () => {