3v324v23's picture
lfs
1e3b872
import { app } from '../../../scripts/app.js'
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)
}
injectCSS(`
.hidden{
display:none !important
}`)
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'
}
}
function videoUpload (node, inputName, inputData, app) {
const imageWidget = node.widgets.find(w => w.name === 'video')
let uploadWidget
const widget = {
type: 'div',
name: 'upload-preview',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 220, node.size[1]),
{
outline: '1px solid'
}
)
}
}
widget.div = $el('div', {})
widget.div.style.width = `120px`
document.body.appendChild(widget.div)
node.addCustomWidget(widget)
// console.log('#imageWidget', imageWidget)
const displayDiv = document.createElement('video')
displayDiv.controls = true
// displayDiv.style=`width:200px;height:200px`
imageWidget.callback = () => {
displayDiv.src = `/view?filename=${
imageWidget.value
}&type=input&subfolder=${''}&rand=${Math.random()}`
// displayDiv.onloadedmetadata = function () {
// var frameCount = displayDiv.duration * displayDiv.webkitDecodedFrameCount
// console.log('视频帧数:' + frameCount)
// node.widgets.filter(w => w.name == 'video_segment_frames')[0].value =
// frameCount
// }
}
if (imageWidget.value) {
// console.log(imageWidget.value)
displayDiv.src = `/view?filename=${
imageWidget.value
}&type=input&subfolder=${''}&rand=${Math.random()}`
}
widget.div.appendChild(displayDiv)
const onRemoved = node.onRemoved
node.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
var default_value = imageWidget.value
Object.defineProperty(imageWidget, 'value', {
set: function (value) {
this._real_value = value
},
get: function () {
let value = ''
if (this._real_value) {
value = this._real_value
} else {
return default_value
}
if (value.filename) {
let real_value = value
value = ''
if (real_value.subfolder) {
value = real_value.subfolder + '/'
}
value += real_value.filename
if (real_value.type && real_value.type !== 'input')
value += ` [${real_value.type}]`
}
return value
}
})
async function uploadFile (file, updateNode, pasted = false) {
try {
// Wrap file in formdata so it includes filename
const body = new FormData()
body.append('image', file)
if (pasted) body.append('subfolder', 'pasted')
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status === 200) {
const data = await resp.json()
// Add the file to the dropdown list and update the widget value
let path = data.name
if (data.subfolder) path = data.subfolder + '/' + path
if (!imageWidget.options.values.includes(path)) {
imageWidget.options.values.push(path)
}
if (updateNode) {
imageWidget.value = path
}
return `/view?filename=${path}&type=input&subfolder=${
pasted ? 'pasted' : ''
}&rand=${Math.random()}`
} else {
alert(resp.status + ' - ' + resp.statusText)
}
} catch (error) {
alert(error)
}
}
const fileInput = document.createElement('input')
Object.assign(fileInput, {
type: 'file',
accept: 'video/*,.mkv,video/webm,video/mp4,video/x-matroska,image/gif',
style: 'display: none',
onchange: async () => {
if (fileInput.files.length) {
let file = fileInput.files[0]
const url = await uploadFile(file, true)
// console.log('fileInput', file)
var reader = new FileReader()
reader.onload = function () {
displayDiv.src = url
displayDiv.onloadedmetadata = function () {
// var frameCount =
// displayDiv.duration * displayDiv.webkitDecodedFrameCount
// console.log('视频帧数:' + frameCount)
// node.widgets.filter(
// w => w.name == 'video_segment_frames'
// )[0].value = frameCount
}
}
reader.readAsDataURL(file)
}
}
})
document.body.append(fileInput)
// Create the button widget for selecting the files
uploadWidget = node.addWidget('button', 'upload file', 'video', () => {
fileInput.click()
})
uploadWidget.serialize = false
return { widget: uploadWidget }
}
ComfyWidgets.VIDEOUPLOAD_ = videoUpload
app.registerExtension({
name: 'Mixlab.Video.LoadVideoAndSegment_',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData?.name == 'LoadVideoAndSegment_') {
nodeData.input.required.upload = ['VIDEOUPLOAD_']
}
},
async loadedGraphNode (node, app) {
if (node.type === 'LoadVideoAndSegment_') {
const imageWidget = node.widgets.find(w => w.name === 'video')
const uploadPreview = node.widgets.find(w => w.name === 'upload-preview')
if (imageWidget.value) {
// console.log(imageWidget.value)
uploadPreview.div.querySelector('video').src = `/view?filename=${
imageWidget.value
}&type=input&subfolder=${''}&rand=${Math.random()}`
}
}
}
})
function offsetDOMWidget (widget, ctx, node, widgetWidth, widgetY, height) {
const margin = 10
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(0, widgetY + margin)
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
Object.assign(widget.inputEl.style, {
transformOrigin: '0 0',
transform: scale,
left: `${transform.e}px`,
top: `${transform.d + transform.f}px`,
width: `${widgetWidth}px`,
height: `${(height || widget.parent?.inputHeight || 32) - margin}px`,
position: 'absolute',
background: !node.color ? '' : node.color,
color: !node.color ? '' : 'white',
zIndex: 5 //app.graph._nodes.indexOf(node),
})
}
export const hasWidgets = node => {
if (!node.widgets || !node.widgets?.[Symbol.iterator]) {
return false
}
return true
}
export const cleanupNode = node => {
if (!hasWidgets(node)) {
return
}
for (const w of node.widgets) {
if (w.canvas) {
w.canvas.remove()
}
if (w.inputEl) {
w.inputEl.remove()
}
// calls the widget remove callback
w.onRemoved?.()
}
}
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()
}
}
}
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',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData?.name == 'ImageListReplace_') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const widget = {
type: 'div',
name: 'preview',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 188, node.size[1]),
{
outline: '1px solid',
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
justifyContent: 'flex-start'
}
)
}
}
widget.div = $el('div', {})
widget.div.style.width = `120px`
widget.div.className = 'hidden'
document.body.appendChild(widget.div)
this.addCustomWidget(widget)
// console.log('#ImageListReplace', widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
// let _image_replace = message._image_replace[0]
// _image_replace = `/view?filename=${_image_replace.filename}&type=${
// _image_replace.type
// }&subfolder=${_image_replace.subfolder}&rand=${Math.random()}`
let preview = this.widgets.filter(w => w.name == 'preview')[0]
if (message._images.length > 0) {
preview.div.className = ''
// console.log('#ImageListReplace', preview.div)
}
preview.div.innerHTML = ''
for (const img_ of message._images) {
let img = new Image()
img.style = `width: 100px;
margin: 4px;`
img.src = `/view?filename=${img_.filename}&type=${
img_.type
}&subfolder=${img_.subfolder}&rand=${Math.random()}`
preview.div.appendChild(img)
}
let start_index = this.widgets.filter(w => w.name == 'start_index')[0]
let end_index = this.widgets.filter(w => w.name == 'end_index')[0]
let invert = this.widgets.filter(w => w.name == 'invert')[0]
let _sc = start_index.callback.bind(start_index)
let _ec = end_index.callback.bind(end_index)
const selectImages = () => {
// console.log(v)
let s = start_index.value,
e = end_index.value
let imgs = preview.div.querySelectorAll('img')
for (let index = 0; index < imgs.length; index++) {
if (invert.value) {
imgs[index].style.outline =
index >= s && index <= e ? 'none' : '4px solid #cbd3fe'
} else {
imgs[index].style.outline =
index >= s && index <= e ? '4px solid #cbd3fe' : 'none'
}
}
}
selectImages()
start_index.callback = v => {
let s = v,
e = end_index.value
let imgs = preview.div.querySelectorAll('img')
for (let index = 0; index < imgs.length; index++) {
if (invert.value) {
imgs[index].style.outline =
index >= s && index <= e ? 'none' : '4px solid #cbd3fe'
} else {
imgs[index].style.outline =
index >= s && index <= e ? '4px solid #cbd3fe' : 'none'
}
}
_sc(v)
}
end_index.callback = v => {
let s = start_index.value,
e = v
let imgs = preview.div.querySelectorAll('img')
for (let index = 0; index < imgs.length; index++) {
if (invert.value) {
imgs[index].style.outline =
index >= s && index <= e ? 'none' : '4px solid #cbd3fe'
} else {
imgs[index].style.outline =
index >= s && index <= e ? '4px solid #cbd3fe' : 'none'
}
}
_ec(v)
}
invert.callback = v => {
selectImages()
}
try {
} catch (error) {}
}
}
if (
nodeData?.name == 'VideoCombine_Adv' ||
nodeData?.name == 'CombineAudioVideo'
) {
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
const prefix = 'vhs_gif_preview_'
const r = onExecuted ? onExecuted.apply(this, message) : undefined
if (this.widgets) {
const pos = this.widgets.findIndex(w => w.name === `${prefix}_0`)
if (pos !== -1) {
for (let i = pos; i < this.widgets.length; i++) {
this.widgets[i].onRemoved?.()
}
this.widgets.length = pos
}
if (message?.gifs) {
message.gifs.forEach((params, i) => {
const previewUrl = api.apiURL(
'/view?' + new URLSearchParams(params).toString()
)
const w = this.addCustomWidget(
createPreviewElement(
`${prefix}_${i}`,
previewUrl,
params.format || 'image/gif'
)
)
w.parent = this
})
}
const onRemoved = this.onRemoved
this.onRemoved = () => {
cleanupNode(this)
return onRemoved?.()
}
}
this.setSize([
this.size[0],
this.computeSize([this.size[0], this.size[1]])[1]
])
return r
}
}
}
})