a better approach to loading comfy workflow without the use of special custom nodes
parent
0ae8bb7906
commit
dda6185bdb
|
|
@ -1185,6 +1185,7 @@
|
|||
<div class="lexicaContainer"></div>
|
||||
<sp-divider class="line-divider" size="large"></sp-divider>
|
||||
<sp-divider class="line-divider" size="large"></sp-divider>
|
||||
<!-- <div id="ComfyUIContainer"></div> -->
|
||||
</div>
|
||||
</uxp-panel>
|
||||
|
||||
|
|
|
|||
5
index.js
5
index.js
|
|
@ -94,6 +94,8 @@ const {
|
|||
lexica,
|
||||
api_ts,
|
||||
comfyui,
|
||||
comfyui_util,
|
||||
diffusion_chain,
|
||||
} = require('./typescripts/dist/bundle')
|
||||
|
||||
const io = require('./utility/io')
|
||||
|
|
@ -1814,3 +1816,6 @@ async function openFileFromUrlExe(url, format = 'gif') {
|
|||
await openFileFromUrl(url, format)
|
||||
})
|
||||
}
|
||||
|
||||
let comfy_server = new diffusion_chain.ComfyServer('http://127.0.0.1:8188')
|
||||
let comfy_object_info = diffusion_chain.ComfyApi.objectInfo(comfy_server)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"@types/react": "^18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"changedpi": "^1.0.4",
|
||||
"diffusion-chain": "file:../diffusion-chain",
|
||||
"fastify": "^4.10.2",
|
||||
"jimp": "^0.16.2",
|
||||
"madge": "^6.0.0",
|
||||
|
|
@ -49,6 +50,16 @@
|
|||
"yazl": "^2.5.1"
|
||||
}
|
||||
},
|
||||
"../diffusion-chain": {
|
||||
"version": "1.0.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^20.4.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
|
||||
|
|
@ -4731,6 +4742,10 @@
|
|||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diffusion-chain": {
|
||||
"resolved": "../diffusion-chain",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
|
|
@ -9272,15 +9287,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
|
||||
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
|
|
@ -13368,6 +13383,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"diffusion-chain": {
|
||||
"version": "file:../diffusion-chain",
|
||||
"requires": {
|
||||
"@types/node": "^20.4.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
},
|
||||
"dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
|
|
@ -16652,9 +16676,9 @@
|
|||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
|
||||
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw=="
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w=="
|
||||
},
|
||||
"undefsafe": {
|
||||
"version": "2.0.5",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"@types/react": "^18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"changedpi": "^1.0.4",
|
||||
"diffusion-chain": "file:../diffusion-chain",
|
||||
"fastify": "^4.10.2",
|
||||
"jimp": "^0.16.2",
|
||||
"madge": "^6.0.0",
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
"@babel/plugin-transform-react-jsx": "^7.21.5",
|
||||
"@svgr/webpack": "^8.0.1",
|
||||
"babel-loader": "^9.1.2",
|
||||
"chalk": "^5.3.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"commander": "^11.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
|
|
@ -45,7 +47,6 @@
|
|||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.82.1",
|
||||
"webpack-cli": "^5.1.1",
|
||||
"chalk": "^5.3.0",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -64,4 +65,4 @@
|
|||
"url": "https://github.com/AbdullahAlfaraj/Auto-Photoshop-StableDiffusion-Plugin/issues"
|
||||
},
|
||||
"homepage": "https://github.com/AbdullahAlfaraj/Auto-Photoshop-StableDiffusion-Plugin#readme"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"1": {
|
||||
"inputs": {
|
||||
"ckpt_name": "cardosAnime_v20.safetensors",
|
||||
"beta_schedule": "sqrt_linear (AnimateDiff)"
|
||||
},
|
||||
"class_type": "CheckpointLoaderSimpleWithNoiseSelect"
|
||||
},
|
||||
"2": {
|
||||
"inputs": {
|
||||
"vae_name": "MoistMix.vae.pt"
|
||||
},
|
||||
"class_type": "VAELoader"
|
||||
},
|
||||
"3": {
|
||||
"inputs": {
|
||||
"text": "ship in storm, waves, dark, night, Artstation ",
|
||||
"clip": ["4", 0]
|
||||
},
|
||||
"class_type": "CLIPTextEncode"
|
||||
},
|
||||
"4": {
|
||||
"inputs": {
|
||||
"stop_at_clip_layer": -2,
|
||||
"clip": ["1", 1]
|
||||
},
|
||||
"class_type": "CLIPSetLastLayer"
|
||||
},
|
||||
"6": {
|
||||
"inputs": {
|
||||
"text": "(ugly:1.2), (worst quality, low quality: 1.4)",
|
||||
"clip": ["4", 0]
|
||||
},
|
||||
"class_type": "CLIPTextEncode"
|
||||
},
|
||||
"7": {
|
||||
"inputs": {
|
||||
"seed": 711493021904285,
|
||||
"steps": 20,
|
||||
"cfg": 8,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "normal",
|
||||
"denoise": 1,
|
||||
"model": ["8", 0],
|
||||
"positive": ["3", 0],
|
||||
"negative": ["6", 0],
|
||||
"latent_image": ["8", 1]
|
||||
},
|
||||
"class_type": "KSampler"
|
||||
},
|
||||
"8": {
|
||||
"inputs": {
|
||||
"model_name": "mm_sd_v14.ckpt",
|
||||
"unlimited_area_hack": true,
|
||||
"model": ["1", 0],
|
||||
"latents": ["9", 0]
|
||||
},
|
||||
"class_type": "AnimateDiffLoaderV1"
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"width": 512,
|
||||
"height": 512,
|
||||
"batch_size": 16
|
||||
},
|
||||
"class_type": "EmptyLatentImage"
|
||||
},
|
||||
"10": {
|
||||
"inputs": {
|
||||
"samples": ["7", 0],
|
||||
"vae": ["2", 0]
|
||||
},
|
||||
"class_type": "VAEDecode"
|
||||
},
|
||||
"12": {
|
||||
"inputs": {
|
||||
"filename_prefix": "AA_readme",
|
||||
"images": ["10", 0]
|
||||
},
|
||||
"class_type": "SaveImage"
|
||||
},
|
||||
"26": {
|
||||
"inputs": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"save_image": "Enabled",
|
||||
"filename_prefix": "AA_readme_gif",
|
||||
"images": ["10", 0]
|
||||
},
|
||||
"class_type": "ADE_AnimateDiffCombine"
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,13 @@ import React from 'react'
|
|||
import ReactDOM from 'react-dom/client'
|
||||
import { requestGet, requestPost } from '../util/ts/api'
|
||||
import { observer } from 'mobx-react'
|
||||
import { runInAction } from 'mobx'
|
||||
import {
|
||||
MoveToCanvasSvg,
|
||||
SliderType,
|
||||
SpMenu,
|
||||
SpSlider,
|
||||
SpSliderWithLabel,
|
||||
SpTextfield,
|
||||
} from '../util/elements'
|
||||
import { ErrorBoundary } from '../util/errorBoundary'
|
||||
|
|
@ -13,15 +16,15 @@ import { Collapsible } from '../util/collapsible'
|
|||
import Locale from '../locale/locale'
|
||||
import { AStore } from '../main/astore'
|
||||
|
||||
import hi_res_prompt from './prompt.json'
|
||||
import { Grid } from '../util/grid'
|
||||
import { io } from '../util/oldSystem'
|
||||
import { app } from 'photoshop'
|
||||
import { reaction, toJS } from 'mobx'
|
||||
import { storage } from 'uxp'
|
||||
export let hi_res_prompt_temp = hi_res_prompt
|
||||
console.log('hi_res_prompt: ', hi_res_prompt)
|
||||
|
||||
import util from './util'
|
||||
import * as diffusion_chain from 'diffusion-chain'
|
||||
import { urlToCanvas } from '../util/ts/general'
|
||||
interface Error {
|
||||
type: string
|
||||
message: string
|
||||
|
|
@ -40,49 +43,6 @@ interface Result {
|
|||
node_errors?: { [key: string]: NodeError }
|
||||
}
|
||||
|
||||
const result: Result = {
|
||||
error: {
|
||||
type: 'prompt_outputs_failed_validation',
|
||||
message: 'Prompt outputs failed validation',
|
||||
details: '',
|
||||
extra_info: {},
|
||||
},
|
||||
node_errors: {
|
||||
'16': {
|
||||
errors: [
|
||||
{
|
||||
type: 'value_not_in_list',
|
||||
message: 'Value not in list',
|
||||
details:
|
||||
"ckpt_name: 'v2-1_768-ema-pruned.ckpt' not in ['anythingV5Anything_anythingV5PrtRE.safetensors', 'deliberate_v2.safetensors', 'dreamshaper_631BakedVae.safetensors', 'dreamshaper_631Inpainting.safetensors', 'edgeOfRealism_eorV20Fp16BakedVAE.safetensors', 'juggernaut_final-inpainting.safetensors', 'juggernaut_final.safetensors', 'loraChGirl.safetensors', 'sd-v1-5-inpainting.ckpt', 'sd_xl_base_1.0.safetensors', 'sd_xl_refiner_1.0.safetensors', 'v1-5-pruned-emaonly.ckpt']",
|
||||
extra_info: {
|
||||
input_name: 'ckpt_name',
|
||||
input_config: [
|
||||
[
|
||||
'anythingV5Anything_anythingV5PrtRE.safetensors',
|
||||
'deliberate_v2.safetensors',
|
||||
'dreamshaper_631BakedVae.safetensors',
|
||||
'dreamshaper_631Inpainting.safetensors',
|
||||
'edgeOfRealism_eorV20Fp16BakedVAE.safetensors',
|
||||
'juggernaut_final-inpainting.safetensors',
|
||||
'juggernaut_final.safetensors',
|
||||
'loraChGirl.safetensors',
|
||||
'sd-v1-5-inpainting.ckpt',
|
||||
'sd_xl_base_1.0.safetensors',
|
||||
'sd_xl_refiner_1.0.safetensors',
|
||||
'v1-5-pruned-emaonly.ckpt',
|
||||
],
|
||||
],
|
||||
received_value: 'v2-1_768-ema-pruned.ckpt',
|
||||
},
|
||||
},
|
||||
],
|
||||
dependent_outputs: ['9', '12'],
|
||||
class_type: 'CheckpointLoaderSimple',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function logError(result: Result) {
|
||||
// Top-level error
|
||||
let has_error = false
|
||||
|
|
@ -179,6 +139,7 @@ export async function generateRequest(prompt: any) {
|
|||
export async function generateImage(prompt: any) {
|
||||
try {
|
||||
let { history_result, prompt_id }: any = await generateRequest(prompt)
|
||||
|
||||
const outputs: any[] = Object.values(history_result[prompt_id].outputs)
|
||||
const images: any[] = []
|
||||
for (const output of outputs) {
|
||||
|
|
@ -188,6 +149,8 @@ export async function generateImage(prompt: any) {
|
|||
}
|
||||
|
||||
const base64_imgs = []
|
||||
const formats: string[] = []
|
||||
|
||||
for (const image of images) {
|
||||
const img = await loadImage(
|
||||
image.filename,
|
||||
|
|
@ -195,14 +158,20 @@ export async function generateImage(prompt: any) {
|
|||
image.type
|
||||
)
|
||||
base64_imgs.push(img)
|
||||
formats.push(util.getFileFormat(image.filename))
|
||||
}
|
||||
|
||||
store.data.comfyui_output_images = base64_imgs
|
||||
|
||||
const thumbnails = []
|
||||
for (const image of base64_imgs) {
|
||||
thumbnails.push(await io.createThumbnail(image, 300))
|
||||
for (let i = 0; i < base64_imgs.length; ++i) {
|
||||
if (['png', 'webp', 'jpg'].includes(formats[i])) {
|
||||
thumbnails.push(await io.createThumbnail(base64_imgs[i], 300))
|
||||
} else if (['gif'].includes(formats[i])) {
|
||||
thumbnails.push('data:image/gif;base64,' + base64_imgs[i])
|
||||
}
|
||||
}
|
||||
|
||||
store.data.comfyui_output_thumbnail_images = thumbnails
|
||||
|
||||
return base64_imgs
|
||||
|
|
@ -242,6 +211,7 @@ export async function loadImage(
|
|||
}
|
||||
|
||||
export async function getConfig() {
|
||||
//TODO: replace this method with get_object_info from comfyapi
|
||||
try {
|
||||
const prompt = {
|
||||
'1': {
|
||||
|
|
@ -376,11 +346,31 @@ export const store = new AStore({
|
|||
// workflows_paths: [] as string[],
|
||||
// workflows_names: [] as string[],
|
||||
workflows: {} as any,
|
||||
selected_workflow: '', // the selected workflow from the workflow menu
|
||||
selected_workflow_name: '', // the selected workflow from the workflow menu
|
||||
current_prompt: {} as any, // current prompt extracted from the workflow
|
||||
thumbnail_image_size: 100,
|
||||
load_image_nodes: {} as any, //our custom loadImageBase64 nodes, we need to substitute comfyui LoadImage nodes with before generating a prompt
|
||||
// load_image_base64_strings: {} as any, //images the user added to the plugin comfy ui
|
||||
object_info: undefined as any,
|
||||
current_prompt2: {} as any,
|
||||
current_prompt2_output: {} as any,
|
||||
output_thumbnail_image_size: {} as Record<string, number>,
|
||||
comfy_server: new diffusion_chain.ComfyServer(
|
||||
'http://127.0.0.1:8188'
|
||||
) as diffusion_chain.ComfyServer,
|
||||
loaded_images_base64_url: [] as string[],
|
||||
current_loaded_image: {} as Record<string, string>,
|
||||
loaded_images_list: [] as string[], // store an array of all images in the comfy's input directory
|
||||
nodes_order: [] as string[], // nodes with smaller index will be rendered first,
|
||||
can_edit_nodes: false as boolean,
|
||||
nodes_label: {} as Record<string, string>,
|
||||
workflows2: {
|
||||
hi_res_workflow: util.hi_res_workflow,
|
||||
lora_less_workflow: util.lora_less_workflow,
|
||||
img2img_workflow: util.img2img_workflow,
|
||||
animatediff_workflow: util.animatediff_workflow,
|
||||
} as Record<string, any>,
|
||||
progress_value: 0,
|
||||
})
|
||||
|
||||
export function storeToPrompt(store: any, basePrompt: any) {
|
||||
|
|
@ -615,9 +605,9 @@ class ComfyNodeComponent extends React.Component<{}> {
|
|||
label_item="Select a workflow"
|
||||
selected_index={Object.values(
|
||||
store.data.workflows
|
||||
).indexOf(store.data.selected_workflow)}
|
||||
).indexOf(store.data.selected_workflow_name)}
|
||||
onChange={async (id: any, value: any) => {
|
||||
store.data.selected_workflow = value.item
|
||||
store.data.selected_workflow_name = value.item
|
||||
await loadWorkflow(
|
||||
store.data.workflows[value.item]
|
||||
)
|
||||
|
|
@ -745,17 +735,803 @@ class ComfyNodeComponent extends React.Component<{}> {
|
|||
}
|
||||
}
|
||||
|
||||
function setSliderValue(store: any, node_id: string, name: string, value: any) {
|
||||
runInAction(() => {
|
||||
store.data.current_prompt2[node_id].inputs[name] = value
|
||||
})
|
||||
}
|
||||
async function onChangeLoadImage(node_id: string, filename: string) {
|
||||
try {
|
||||
store.data.current_loaded_image[node_id] =
|
||||
await util.base64UrlFromComfy(store.data.comfy_server, {
|
||||
filename: encodeURIComponent(filename),
|
||||
type: 'input',
|
||||
subfolder: '',
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
function renderNode(node_id: string, node: any) {
|
||||
const comfy_node_info = toJS(store.data.object_info[node.class_type])
|
||||
const is_output = comfy_node_info.output_node
|
||||
console.log('comfy_node_info: ', comfy_node_info)
|
||||
const node_type = util.getNodeType(node.class_type)
|
||||
let node_html
|
||||
if (node_type === util.ComfyNodeType.LoadImage) {
|
||||
const loaded_images = store.data.loaded_images_list
|
||||
const inputs = store.data.current_prompt2[node_id].inputs
|
||||
const node_name = node.class_type
|
||||
node_html = (
|
||||
<div>
|
||||
New load image component
|
||||
<sp-label class="title" style={{ display: 'inline-block' }}>
|
||||
{node_name}
|
||||
</sp-label>
|
||||
<div>
|
||||
<SpMenu
|
||||
disabled={store.data.can_edit_nodes ? true : void 0}
|
||||
size="m"
|
||||
title={node_name}
|
||||
items={loaded_images}
|
||||
label_item={`Select an Image`}
|
||||
// id={'model_list'}
|
||||
selected_index={loaded_images.indexOf(inputs.image)}
|
||||
onChange={async (
|
||||
id: any,
|
||||
{ index, item }: Record<string, any>
|
||||
) => {
|
||||
console.log('onChange value.item: ', item)
|
||||
inputs.image = item
|
||||
//load image store for each LoadImage Node
|
||||
//use node_id to store these
|
||||
|
||||
onChangeLoadImage(node_id, item)
|
||||
}}
|
||||
></SpMenu>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
let index: number = loaded_images.indexOf(
|
||||
inputs.image
|
||||
)
|
||||
index--
|
||||
const length = loaded_images.length
|
||||
index = ((index % length) + length) % length
|
||||
inputs.image = loaded_images[index]
|
||||
onChangeLoadImage(node_id, inputs.image)
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{'<'}{' '}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
let index: number = loaded_images.indexOf(
|
||||
inputs.image
|
||||
)
|
||||
index++
|
||||
const length = loaded_images.length
|
||||
index = ((index % length) + length) % length
|
||||
inputs.image = loaded_images[index]
|
||||
onChangeLoadImage(node_id, inputs.image)
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{'>'}{' '}
|
||||
</button>
|
||||
</div>
|
||||
<img
|
||||
src={store.data.current_loaded_image[node_id]}
|
||||
style={{ height: '200px' }}
|
||||
onError={async () => {
|
||||
console.error(
|
||||
'error loading image: ',
|
||||
store.data.current_loaded_image[node_id]
|
||||
)
|
||||
// try {
|
||||
// const filename = inputs.image
|
||||
// store.data.current_loaded_image[node_id] =
|
||||
// await util.base64UrlFromComfy(
|
||||
// store.data.comfy_server,
|
||||
// {
|
||||
// filename: encodeURIComponent(filename),
|
||||
// type: 'input',
|
||||
// subfolder: '',
|
||||
// }
|
||||
// )
|
||||
// console.log(
|
||||
// 'store.data.current_loaded_image[node_id]: ',
|
||||
// toJS(store.data.current_loaded_image[node_id])
|
||||
// )
|
||||
// } catch (e) {
|
||||
// console.warn(e)
|
||||
// }
|
||||
onChangeLoadImage(node_id, inputs.image)
|
||||
}}
|
||||
/>
|
||||
{/* <Grid
|
||||
thumbnails={store.data.loaded_images_base64_url}
|
||||
width={200}
|
||||
height={200}
|
||||
></Grid> */}
|
||||
</div>
|
||||
)
|
||||
} else if (node_type === util.ComfyNodeType.Normal) {
|
||||
node_html = Object.entries(node.inputs).map(([name, value], index) => {
|
||||
// store.data.current_prompt2[node_id].inputs[name] = value
|
||||
try {
|
||||
const input = comfy_node_info.input.required[name]
|
||||
let { type, config } = util.parseComfyInput(input)
|
||||
const html_element = renderInput(
|
||||
node_id,
|
||||
name,
|
||||
type,
|
||||
config,
|
||||
`${node_id}_${name}_${type}_${index}`
|
||||
)
|
||||
return html_element
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (is_output) {
|
||||
const output_node_element = (
|
||||
<div>
|
||||
<SpSlider
|
||||
disabled={store.data.can_edit_nodes ? true : void 0}
|
||||
style={{ display: 'block' }}
|
||||
show-value="false"
|
||||
id="slUpscaleSize"
|
||||
min="100"
|
||||
max="300"
|
||||
value={store.data.output_thumbnail_image_size[node_id]}
|
||||
title=""
|
||||
onInput={(evt: any) => {
|
||||
store.data.output_thumbnail_image_size[node_id] =
|
||||
evt.target.value
|
||||
}}
|
||||
>
|
||||
<sp-label slot="label" class="title">
|
||||
Thumbnail Size:
|
||||
</sp-label>
|
||||
<sp-label class="labelNumber" slot="label">
|
||||
{parseInt(
|
||||
store.data.output_thumbnail_image_size[
|
||||
node_id
|
||||
] as any
|
||||
)}
|
||||
</sp-label>
|
||||
</SpSlider>
|
||||
<Grid
|
||||
// thumbnails_data={store.data.images}
|
||||
|
||||
thumbnails={store.data.current_prompt2_output[node_id]}
|
||||
width={store.data.output_thumbnail_image_size[node_id]}
|
||||
height={200}
|
||||
action_buttons={[
|
||||
{
|
||||
ComponentType: MoveToCanvasSvg,
|
||||
callback: (index: number) => {
|
||||
// io.IO.base64ToLayer(
|
||||
// store.data.current_prompt2_output[node_id][
|
||||
// index
|
||||
// ]
|
||||
// )
|
||||
urlToCanvas(
|
||||
store.data.current_prompt2_output[node_id][
|
||||
index
|
||||
],
|
||||
'comfy_output.png'
|
||||
)
|
||||
},
|
||||
title: 'Copy Image to Canvas',
|
||||
},
|
||||
]}
|
||||
></Grid>
|
||||
</div>
|
||||
)
|
||||
|
||||
return output_node_element
|
||||
}
|
||||
return node_html
|
||||
}
|
||||
function renderInput(
|
||||
node_id: string,
|
||||
name: string,
|
||||
type: any,
|
||||
config: any,
|
||||
key?: string
|
||||
) {
|
||||
let html_element = (
|
||||
<div key={key ?? void 0}>
|
||||
{name},{type}, {JSON.stringify(config)}
|
||||
</div>
|
||||
)
|
||||
const inputs = store.data.current_prompt2[node_id].inputs
|
||||
if (type === util.ComfyInputType.BigNumber) {
|
||||
html_element = (
|
||||
<>
|
||||
<sp-label slot="label">{name}:</sp-label>
|
||||
<sp-textfield
|
||||
disabled={store.data.can_edit_nodes ? true : void 0}
|
||||
// key={key ?? void 0}
|
||||
type="text"
|
||||
// placeholder="cute cats"
|
||||
// value={config.default}
|
||||
value={inputs[name]}
|
||||
onInput={(event: any) => {
|
||||
// store.data.search_query = event.target.value
|
||||
inputs[name] = event.target.value
|
||||
console.log(`${name}: ${event.target.value}`)
|
||||
}}
|
||||
></sp-textfield>
|
||||
</>
|
||||
)
|
||||
} else if (type === util.ComfyInputType.TextFieldNumber) {
|
||||
html_element = (
|
||||
<>
|
||||
<sp-label slot="label">{name}:</sp-label>
|
||||
<SpTextfield
|
||||
disabled={store.data.can_edit_nodes ? true : void 0}
|
||||
type="text"
|
||||
// value={config.default}
|
||||
value={inputs[name]}
|
||||
onChange={(e: any) => {
|
||||
const v = e.target.value
|
||||
let new_value =
|
||||
v !== ''
|
||||
? Math.max(config.min, Math.min(config.max, v))
|
||||
: v
|
||||
inputs[name] = new_value
|
||||
|
||||
console.log(`${name}: ${e.target.value}`)
|
||||
}}
|
||||
></SpTextfield>
|
||||
</>
|
||||
)
|
||||
} else if (type === util.ComfyInputType.Slider) {
|
||||
html_element = (
|
||||
<SpSliderWithLabel
|
||||
// key={key ?? void 0}
|
||||
disabled={store.data.can_edit_nodes ? true : void 0}
|
||||
show-value={false}
|
||||
steps={config?.step ?? 1}
|
||||
out_min={config?.min ?? 0}
|
||||
out_max={config?.max ?? 100}
|
||||
output_value={store.data.current_prompt2[node_id].inputs[name]}
|
||||
label={name}
|
||||
slider_type={
|
||||
config?.step && !Number.isInteger(config?.step)
|
||||
? SliderType.Float
|
||||
: SliderType.Integer
|
||||
}
|
||||
onSliderInput={(new_value: number) => {
|
||||
// inputs[name] = new_value
|
||||
// setSliderValue(store, node_id, name, new_value)
|
||||
store.data.current_prompt2[node_id].inputs[name] = new_value
|
||||
console.log('slider_change: ', new_value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else if (type === util.ComfyInputType.Menu) {
|
||||
html_element = (
|
||||
<>
|
||||
<sp-label class="title" style={{ display: 'inline-block' }}>
|
||||
{name}
|
||||
</sp-label>
|
||||
<SpMenu
|
||||
disabled={store.data.can_edit_nodes ? true : void 0}
|
||||
size="m"
|
||||
title={name}
|
||||
items={config}
|
||||
label_item={`Select a ${name}`}
|
||||
// id={'model_list'}
|
||||
selected_index={config.indexOf(inputs[name])}
|
||||
onChange={(
|
||||
id: any,
|
||||
{ index, item }: Record<string, any>
|
||||
) => {
|
||||
console.log('onChange value.item: ', item)
|
||||
inputs[name] = item
|
||||
}}
|
||||
></SpMenu>
|
||||
</>
|
||||
)
|
||||
} else if (type === util.ComfyInputType.TextArea) {
|
||||
html_element = (
|
||||
<sp-textarea
|
||||
disabled={store.data.can_edit_nodes ? true : void 0}
|
||||
// key={key}
|
||||
onInput={(event: any) => {
|
||||
try {
|
||||
// this.changePositivePrompt(
|
||||
// event.target.value,
|
||||
// store.data.current_index
|
||||
// )
|
||||
// autoResize(
|
||||
// event.target,
|
||||
// store.data.positivePrompts[
|
||||
// store.data.current_index
|
||||
// ]
|
||||
// )
|
||||
inputs[name] = event.target.value
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
}}
|
||||
placeholder={`${name}`}
|
||||
value={inputs[name]}
|
||||
></sp-textarea>
|
||||
)
|
||||
} else if (type === util.ComfyInputType.TextField) {
|
||||
html_element = (
|
||||
<>
|
||||
<sp-label slot="label">{name}:</sp-label>
|
||||
|
||||
<SpTextfield
|
||||
disabled={store.data.can_edit_nodes ? true : void 0}
|
||||
type="text"
|
||||
// value={config.default}
|
||||
value={inputs[name]}
|
||||
onChange={(e: any) => {
|
||||
inputs[name] = e.target.value
|
||||
console.log(`${name}: ${e.target.value}`)
|
||||
}}
|
||||
></SpTextfield>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return <div key={key}>{html_element}</div>
|
||||
}
|
||||
|
||||
export function swap(index1: number, index2: number) {
|
||||
const { length } = store.data.nodes_order
|
||||
if (index1 >= 0 && index1 < length && index2 >= 0 && index2 < length) {
|
||||
;[store.data.nodes_order[index1], store.data.nodes_order[index2]] = [
|
||||
store.data.nodes_order[index2],
|
||||
store.data.nodes_order[index1],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export function saveWorkflowData(
|
||||
workflow_name: string,
|
||||
{ prompt, nodes_order, nodes_label }: WorkflowData
|
||||
) {
|
||||
storage.localStorage.setItem(
|
||||
workflow_name,
|
||||
JSON.stringify({ prompt, nodes_order, nodes_label })
|
||||
)
|
||||
}
|
||||
export function loadWorkflowData(workflow_name: string): WorkflowData {
|
||||
const workflow_data: WorkflowData = JSON.parse(
|
||||
storage.localStorage.getItem(workflow_name)
|
||||
)
|
||||
return workflow_data
|
||||
}
|
||||
interface WorkflowData {
|
||||
prompt: any
|
||||
nodes_order: string[]
|
||||
nodes_label: Record<string, string>
|
||||
}
|
||||
function loadWorkflow2(workflow: any) {
|
||||
const copyJson = (originalObject: any) =>
|
||||
JSON.parse(JSON.stringify(originalObject))
|
||||
//1) get prompt
|
||||
store.data.current_prompt2 = copyJson(workflow)
|
||||
|
||||
//2) get the original order
|
||||
store.data.nodes_order = Object.keys(toJS(store.data.current_prompt2))
|
||||
|
||||
//3) get labels for each nodes
|
||||
store.data.nodes_label = Object.fromEntries(
|
||||
Object.entries(toJS(store.data.current_prompt2)).map(
|
||||
([node_id, node]: [string, any]) => {
|
||||
return [
|
||||
node_id,
|
||||
toJS(store.data.object_info[node.class_type]).display_name,
|
||||
]
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// parse the output nodes
|
||||
// Note: we can't modify the node directly in the prompt like we do for input nodes.
|
||||
//.. since this data doesn't exist on the prompt. so we create separate container for the output images
|
||||
store.data.current_prompt2_output = Object.entries(
|
||||
store.data.current_prompt2
|
||||
).reduce(
|
||||
(
|
||||
output_entries: Record<string, any[]>,
|
||||
[node_id, node]: [string, any]
|
||||
) => {
|
||||
if (store.data.object_info[node.class_type].output_node) {
|
||||
output_entries[node_id] = []
|
||||
}
|
||||
return output_entries
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
//slider variables for output nodes
|
||||
//TODO: delete store.data.output_thumbnail_image_size before loading a new workflow
|
||||
for (let key in toJS(store.data.current_prompt2_output)) {
|
||||
store.data.output_thumbnail_image_size[key] = 200
|
||||
}
|
||||
|
||||
const workflow_name = store.data.selected_workflow_name
|
||||
if (workflow_name) {
|
||||
// check if the workflow has a name
|
||||
|
||||
if (workflow_name in storage.localStorage) {
|
||||
//load the workflow data from local storage
|
||||
//1) load the last parameters used in generation
|
||||
//2) load the order of the nodes
|
||||
//3) load the labels of the nodes
|
||||
|
||||
const workflow_data: WorkflowData = loadWorkflowData(workflow_name)
|
||||
if (
|
||||
util.isSameStructure(
|
||||
workflow_data.prompt,
|
||||
toJS(store.data.current_prompt2)
|
||||
)
|
||||
) {
|
||||
//load 1)
|
||||
store.data.current_prompt2 = workflow_data.prompt
|
||||
//load 2)
|
||||
store.data.nodes_order = workflow_data.nodes_order
|
||||
//load 3)
|
||||
store.data.nodes_label = workflow_data.nodes_label
|
||||
} else {
|
||||
// do not load. instead override the localStorage with the new values
|
||||
workflow_data.prompt = toJS(store.data.current_prompt2)
|
||||
workflow_data.nodes_order = toJS(store.data.nodes_order)
|
||||
workflow_data.nodes_label = toJS(store.data.nodes_label)
|
||||
|
||||
saveWorkflowData(workflow_name, workflow_data)
|
||||
}
|
||||
} else {
|
||||
// if workflow data is missing from local storage then save it for next time.
|
||||
//1) save parameters values
|
||||
//2) save nodes order
|
||||
//3) save nodes label
|
||||
|
||||
const prompt = toJS(store.data.current_prompt2)
|
||||
const nodes_order = toJS(store.data.nodes_order)
|
||||
const nodes_label = toJS(store.data.nodes_label)
|
||||
saveWorkflowData(workflow_name, {
|
||||
prompt,
|
||||
nodes_order,
|
||||
nodes_label,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@observer
|
||||
class ComfyWorkflowComponent extends React.Component<{}, { value?: number }> {
|
||||
async componentDidMount(): Promise<void> {
|
||||
try {
|
||||
store.data.object_info = await diffusion_chain.ComfyApi.objectInfo(
|
||||
store.data.comfy_server
|
||||
)
|
||||
|
||||
loadWorkflow2(util.lora_less_workflow)
|
||||
|
||||
//convert all of comfyui loaded images into base64url that the plugin can use
|
||||
const loaded_images =
|
||||
store.data.object_info.LoadImage.input.required['image'][0]
|
||||
const loaded_images_base64_url = await Promise.all(
|
||||
loaded_images.map(async (filename: string) => {
|
||||
try {
|
||||
return await util.base64UrlFromComfy(
|
||||
store.data.comfy_server,
|
||||
{
|
||||
filename: encodeURIComponent(filename),
|
||||
type: 'input',
|
||||
subfolder: '',
|
||||
}
|
||||
)
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
})
|
||||
)
|
||||
store.data.loaded_images_list =
|
||||
store.data.object_info.LoadImage.input.required['image'][0]
|
||||
|
||||
store.data.loaded_images_base64_url = loaded_images_base64_url
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const comfy_server = store.data.comfy_server
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'space-between' }}
|
||||
>
|
||||
{/* {util.getNodes(util.hi_res_workflow).map((node, index) => {
|
||||
// return <div>{node.class_type}</div>
|
||||
return (
|
||||
<div key={'node_' + index}>{this.renderNode(node)}</div>
|
||||
)
|
||||
})} */}
|
||||
<button
|
||||
className="btnSquare"
|
||||
onClick={async () => {
|
||||
// let interval
|
||||
let interval: NodeJS.Timeout = setInterval(
|
||||
function () {
|
||||
store.data.progress_value++
|
||||
console.log(store.data.progress_value)
|
||||
},
|
||||
1000
|
||||
) // 1000 milliseconds = 1 second
|
||||
try {
|
||||
// Start the progress update
|
||||
|
||||
let store_output =
|
||||
await util.postPromptAndGetBase64JsonResult(
|
||||
comfy_server,
|
||||
toJS(store.data.current_prompt2)
|
||||
)
|
||||
store.data.current_prompt2_output =
|
||||
store_output ?? {}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
clearInterval(interval as NodeJS.Timeout)
|
||||
store.data.progress_value = 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
<button
|
||||
className="btnSquare"
|
||||
onClick={async () => {
|
||||
store.data.can_edit_nodes =
|
||||
!store.data.can_edit_nodes
|
||||
|
||||
const workflow_name =
|
||||
store.data.selected_workflow_name
|
||||
const prompt = toJS(store.data.current_prompt2)
|
||||
const nodes_order = toJS(store.data.nodes_order)
|
||||
const nodes_label = toJS(store.data.nodes_label)
|
||||
|
||||
saveWorkflowData(workflow_name, {
|
||||
prompt,
|
||||
nodes_order,
|
||||
nodes_label,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{store.data.can_edit_nodes
|
||||
? 'Done Editing'
|
||||
: 'Edit Nodes'}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<sp-progressbar
|
||||
class="pProgressBars preview_progress_bar"
|
||||
max="100"
|
||||
value={`${store.data.progress_value}`}
|
||||
></sp-progressbar>
|
||||
</div>
|
||||
<div>
|
||||
<SpMenu
|
||||
size="m"
|
||||
title="workflows"
|
||||
items={Object.keys(store.data.workflows2)}
|
||||
label_item="Select a workflow"
|
||||
selected_index={Object.values(
|
||||
store.data.workflows2
|
||||
).indexOf(store.data.selected_workflow_name)}
|
||||
onChange={async (id: any, value: any) => {
|
||||
store.data.selected_workflow_name = value.item
|
||||
loadWorkflow2(store.data.workflows2[value.item])
|
||||
}}
|
||||
></SpMenu>{' '}
|
||||
<button
|
||||
className="btnSquare refreshButton"
|
||||
id="btnResetSettings"
|
||||
title="Refresh the ADetailer Extension"
|
||||
onClick={async () => {
|
||||
// await getConfig()
|
||||
}}
|
||||
></button>
|
||||
</div>
|
||||
|
||||
{store.data.object_info ? (
|
||||
<>
|
||||
<div>
|
||||
{util
|
||||
.getNodes(store.data.current_prompt2)
|
||||
.sort(
|
||||
([node_id1, node1], [node_id2, node2]) => {
|
||||
return (
|
||||
store.data.nodes_order.indexOf(
|
||||
node_id1
|
||||
) -
|
||||
store.data.nodes_order.indexOf(
|
||||
node_id2
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
.map(([node_id, node], index) => {
|
||||
return (
|
||||
<div
|
||||
key={`node_${node_id}_${index}`}
|
||||
style={{
|
||||
border: '2px solid #6d6c6c',
|
||||
|
||||
padding: '3px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: store.data
|
||||
.can_edit_nodes
|
||||
? 'flex'
|
||||
: 'none',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
id={`${node_id}_swap_up`}
|
||||
style={{
|
||||
width: '26px',
|
||||
}}
|
||||
className="btnSquare"
|
||||
onClick={(e: any) => {
|
||||
console.log(
|
||||
'node_id assign to the swap button: ',
|
||||
node_id
|
||||
)
|
||||
swap(
|
||||
index,
|
||||
index - 1
|
||||
)
|
||||
|
||||
// setTimeout(() => {
|
||||
// // e.target.scrollIntoView()
|
||||
|
||||
// ;(
|
||||
// document.getElementById(
|
||||
// `${node_id}_swap_up`
|
||||
// ) as any
|
||||
// ).scrollIntoView(
|
||||
// false
|
||||
// )
|
||||
// }, 200)
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{'▲'}{' '}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
id={`${node_id}_swap_down`}
|
||||
style={{
|
||||
width: '26px',
|
||||
}}
|
||||
className="btnSquare"
|
||||
onClick={() => {
|
||||
swap(
|
||||
index,
|
||||
index + 1
|
||||
)
|
||||
// setTimeout(() => {
|
||||
// ;(
|
||||
// document.getElementById(
|
||||
// `${node_id}_swap_down`
|
||||
// ) as any
|
||||
// ).scrollIntoView()
|
||||
// }, 200)
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{'▼'}{' '}
|
||||
</button>
|
||||
{/* <span ></span> */}
|
||||
</div>
|
||||
</div>
|
||||
<sp-label>
|
||||
"{node_id}":{' '}
|
||||
<span
|
||||
style={{
|
||||
color: store.data
|
||||
.can_edit_nodes
|
||||
? 'white'
|
||||
: void 0,
|
||||
}}
|
||||
>
|
||||
{
|
||||
store.data.nodes_label[
|
||||
node_id
|
||||
]
|
||||
}
|
||||
</span>{' '}
|
||||
</sp-label>{' '}
|
||||
<sp-label
|
||||
style={{
|
||||
display: store.data
|
||||
.can_edit_nodes
|
||||
? void 0
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
{node.class_type}
|
||||
</sp-label>
|
||||
<div
|
||||
style={{
|
||||
display: !store.data
|
||||
.can_edit_nodes
|
||||
? 'none'
|
||||
: void 0,
|
||||
}}
|
||||
>
|
||||
<sp-textfield
|
||||
type="text"
|
||||
placeholder="write a node label"
|
||||
value={
|
||||
store.data.nodes_label[
|
||||
node_id
|
||||
]
|
||||
}
|
||||
onInput={(event: any) => {
|
||||
store.data.nodes_label[
|
||||
node_id
|
||||
] = event.target.value
|
||||
}}
|
||||
></sp-textfield>
|
||||
</div>
|
||||
{renderNode(node_id, node)}
|
||||
{/* <sp-divider
|
||||
class="line-divider"
|
||||
size="large"
|
||||
></sp-divider>
|
||||
<sp-divider
|
||||
class="line-divider"
|
||||
size="large"
|
||||
></sp-divider> */}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
void 0
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
const container = document.getElementById('ComfyUIContainer')!
|
||||
const root = ReactDOM.createRoot(container)
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ErrorBoundary>
|
||||
<div style={{ border: '2px solid #6d6c6c', padding: '3px' }}>
|
||||
<Collapsible defaultIsOpen={true} label={Locale('ComfyUI')}>
|
||||
<ComfyNodeComponent></ComfyNodeComponent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
{/* <ComfyNodeComponent></ComfyNodeComponent> */}
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>
|
||||
//<React.StrictMode>
|
||||
<ErrorBoundary>
|
||||
<div style={{ border: '2px solid #6d6c6c', padding: '3px' }}>
|
||||
<Collapsible defaultIsOpen={true} label={Locale('ComfyUI')}>
|
||||
{/* <ComfyNodeComponent></ComfyNodeComponent> */}
|
||||
|
||||
<ComfyWorkflowComponent />
|
||||
</Collapsible>
|
||||
</div>
|
||||
{/* <ComfyNodeComponent></ComfyNodeComponent> */}
|
||||
</ErrorBoundary>
|
||||
//</React.StrictMode>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,22 +7,10 @@
|
|||
"sampler_name": "dpmpp_sde",
|
||||
"scheduler": "normal",
|
||||
"denoise": 1,
|
||||
"model": [
|
||||
"16",
|
||||
0
|
||||
],
|
||||
"positive": [
|
||||
"6",
|
||||
0
|
||||
],
|
||||
"negative": [
|
||||
"7",
|
||||
0
|
||||
],
|
||||
"latent_image": [
|
||||
"5",
|
||||
0
|
||||
]
|
||||
"model": ["16", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["5", 0]
|
||||
},
|
||||
"class_type": "KSampler"
|
||||
},
|
||||
|
|
@ -37,43 +25,28 @@
|
|||
"6": {
|
||||
"inputs": {
|
||||
"text": "masterpiece HDR victorian portrait painting of woman, blonde hair, mountain nature, blue sky\n",
|
||||
"clip": [
|
||||
"16",
|
||||
1
|
||||
]
|
||||
"clip": ["16", 1]
|
||||
},
|
||||
"class_type": "CLIPTextEncode"
|
||||
},
|
||||
"7": {
|
||||
"inputs": {
|
||||
"text": "bad hands, text, watermark\n",
|
||||
"clip": [
|
||||
"16",
|
||||
1
|
||||
]
|
||||
"clip": ["16", 1]
|
||||
},
|
||||
"class_type": "CLIPTextEncode"
|
||||
},
|
||||
"8": {
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"3",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"16",
|
||||
2
|
||||
]
|
||||
"samples": ["3", 0],
|
||||
"vae": ["16", 2]
|
||||
},
|
||||
"class_type": "VAEDecode"
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"filename_prefix": "ComfyUI",
|
||||
"images": [
|
||||
"8",
|
||||
0
|
||||
]
|
||||
"images": ["8", 0]
|
||||
},
|
||||
"class_type": "SaveImage"
|
||||
},
|
||||
|
|
@ -83,10 +56,7 @@
|
|||
"width": 1152,
|
||||
"height": 1152,
|
||||
"crop": "disabled",
|
||||
"samples": [
|
||||
"3",
|
||||
0
|
||||
]
|
||||
"samples": ["3", 0]
|
||||
},
|
||||
"class_type": "LatentUpscale"
|
||||
},
|
||||
|
|
@ -98,45 +68,24 @@
|
|||
"sampler_name": "dpmpp_2m",
|
||||
"scheduler": "simple",
|
||||
"denoise": 0.5,
|
||||
"model": [
|
||||
"16",
|
||||
0
|
||||
],
|
||||
"positive": [
|
||||
"6",
|
||||
0
|
||||
],
|
||||
"negative": [
|
||||
"7",
|
||||
0
|
||||
],
|
||||
"latent_image": [
|
||||
"10",
|
||||
0
|
||||
]
|
||||
"model": ["16", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["10", 0]
|
||||
},
|
||||
"class_type": "KSampler"
|
||||
},
|
||||
"12": {
|
||||
"inputs": {
|
||||
"filename_prefix": "ComfyUI",
|
||||
"images": [
|
||||
"13",
|
||||
0
|
||||
]
|
||||
"images": ["13", 0]
|
||||
},
|
||||
"class_type": "SaveImage"
|
||||
},
|
||||
"13": {
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"11",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"16",
|
||||
2
|
||||
]
|
||||
"samples": ["11", 0],
|
||||
"vae": ["16", 2]
|
||||
},
|
||||
"class_type": "VAEDecode"
|
||||
},
|
||||
|
|
@ -146,4 +95,4 @@
|
|||
},
|
||||
"class_type": "CheckpointLoaderSimple"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"3": {
|
||||
"inputs": {
|
||||
"seed": 280823642470253,
|
||||
"steps": 20,
|
||||
"cfg": 8,
|
||||
"sampler_name": "dpmpp_2m",
|
||||
"scheduler": "normal",
|
||||
"denoise": 0.8700000000000001,
|
||||
"model": ["14", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["12", 0]
|
||||
},
|
||||
"class_type": "KSampler"
|
||||
},
|
||||
"6": {
|
||||
"inputs": {
|
||||
"text": "photograph of victorian woman with wings, sky clouds, meadow grass\n",
|
||||
"clip": ["14", 1]
|
||||
},
|
||||
"class_type": "CLIPTextEncode"
|
||||
},
|
||||
"7": {
|
||||
"inputs": {
|
||||
"text": "watermark, text\n",
|
||||
"clip": ["14", 1]
|
||||
},
|
||||
"class_type": "CLIPTextEncode"
|
||||
},
|
||||
"8": {
|
||||
"inputs": {
|
||||
"samples": ["3", 0],
|
||||
"vae": ["14", 2]
|
||||
},
|
||||
"class_type": "VAEDecode"
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"filename_prefix": "ComfyUI",
|
||||
"images": ["8", 0]
|
||||
},
|
||||
"class_type": "SaveImage"
|
||||
},
|
||||
"10": {
|
||||
"inputs": {
|
||||
"image": "example.png",
|
||||
"choose file to upload": "image"
|
||||
},
|
||||
"class_type": "LoadImage"
|
||||
},
|
||||
"12": {
|
||||
"inputs": {
|
||||
"pixels": ["10", 0],
|
||||
"vae": ["14", 2]
|
||||
},
|
||||
"class_type": "VAEEncode"
|
||||
},
|
||||
"14": {
|
||||
"inputs": {
|
||||
"ckpt_name": "v1-5-pruned-emaonly.ckpt"
|
||||
},
|
||||
"class_type": "CheckpointLoaderSimple"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
{
|
||||
"8": {
|
||||
"inputs": {
|
||||
"vae_name": "klF8Anime2VAE_klF8Anime2VAE.ckpt"
|
||||
},
|
||||
"class_type": "VAELoader"
|
||||
},
|
||||
"16": {
|
||||
"inputs": {
|
||||
"ckpt_name": "juggernaut_final.safetensors"
|
||||
},
|
||||
"class_type": "CheckpointLoaderSimple"
|
||||
},
|
||||
"18": {
|
||||
"inputs": {
|
||||
"text": "(front view:1.2)",
|
||||
"clip": ["16", 1]
|
||||
},
|
||||
"class_type": "CLIPTextEncode"
|
||||
},
|
||||
"21": {
|
||||
"inputs": {
|
||||
"width": 512,
|
||||
"height": 512,
|
||||
"batch_size": 2
|
||||
},
|
||||
"class_type": "EmptyLatentImage"
|
||||
},
|
||||
"30": {
|
||||
"inputs": {
|
||||
"text": "(a dog sitting:1.3) on a tile floor with a blue eyes and a white nose and tail, looking at the camera, artist, extremely detailed oil painting, a photorealistic painting, photorealism",
|
||||
"clip": ["16", 1]
|
||||
},
|
||||
"class_type": "CLIPTextEncode"
|
||||
},
|
||||
"31": {
|
||||
"inputs": {
|
||||
"tile_size": 512,
|
||||
"samples": ["97", 0],
|
||||
"vae": ["8", 0]
|
||||
},
|
||||
"class_type": "VAEDecodeTiled"
|
||||
},
|
||||
"48": {
|
||||
"inputs": {
|
||||
"weight": ["163", 0],
|
||||
"model_name": "ip-adapter-plus_sd15.bin",
|
||||
"dtype": "fp32",
|
||||
"model": ["162", 0],
|
||||
"image": ["168", 0],
|
||||
"clip_vision": ["57", 0]
|
||||
},
|
||||
"class_type": "IPAdapter"
|
||||
},
|
||||
"50": {
|
||||
"inputs": {
|
||||
"strength": ["163", 0],
|
||||
"noise_augmentation": 0,
|
||||
"conditioning": ["30", 0],
|
||||
"clip_vision_output": ["48", 1]
|
||||
},
|
||||
"class_type": "unCLIPConditioning"
|
||||
},
|
||||
"57": {
|
||||
"inputs": {
|
||||
"clip_name": "model.safetensors"
|
||||
},
|
||||
"class_type": "CLIPVisionLoader"
|
||||
},
|
||||
"97": {
|
||||
"inputs": {
|
||||
"seed": 229741993160779,
|
||||
"steps": 32,
|
||||
"cfg": 6.5,
|
||||
"sampler_name": "dpmpp_2s_ancestral",
|
||||
"scheduler": "karras",
|
||||
"denoise": 1,
|
||||
"model": ["48", 0],
|
||||
"positive": ["50", 0],
|
||||
"negative": ["18", 0],
|
||||
"latent_image": ["21", 0]
|
||||
},
|
||||
"class_type": "KSampler"
|
||||
},
|
||||
"122": {
|
||||
"inputs": {
|
||||
"images": ["31", 0]
|
||||
},
|
||||
"class_type": "PreviewImage"
|
||||
},
|
||||
"155": {
|
||||
"inputs": {
|
||||
"seed": 216203953003378,
|
||||
"steps": 40,
|
||||
"cfg": 5,
|
||||
"sampler_name": "ddim",
|
||||
"scheduler": "normal",
|
||||
"denoise": 0.45,
|
||||
"model": ["48", 0],
|
||||
"positive": ["50", 0],
|
||||
"negative": ["18", 0],
|
||||
"latent_image": ["156", 0]
|
||||
},
|
||||
"class_type": "KSampler"
|
||||
},
|
||||
"156": {
|
||||
"inputs": {
|
||||
"tile_size": 640,
|
||||
"pixels": ["159", 0],
|
||||
"vae": ["8", 0]
|
||||
},
|
||||
"class_type": "VAEEncodeTiled"
|
||||
},
|
||||
"157": {
|
||||
"inputs": {
|
||||
"upscale_model": ["158", 0],
|
||||
"image": ["31", 0]
|
||||
},
|
||||
"class_type": "ImageUpscaleWithModel"
|
||||
},
|
||||
"158": {
|
||||
"inputs": {
|
||||
"model_name": "RealESRGAN_x4plus_anime_6B.pth"
|
||||
},
|
||||
"class_type": "UpscaleModelLoader"
|
||||
},
|
||||
"159": {
|
||||
"inputs": {
|
||||
"upscale_method": "nearest-exact",
|
||||
"scale_by": 0.45,
|
||||
"image": ["157", 0]
|
||||
},
|
||||
"class_type": "ImageScaleBy"
|
||||
},
|
||||
"160": {
|
||||
"inputs": {
|
||||
"filename_prefix": "ComfyUI",
|
||||
"images": ["161", 0]
|
||||
},
|
||||
"class_type": "SaveImage"
|
||||
},
|
||||
"161": {
|
||||
"inputs": {
|
||||
"tile_size": 512,
|
||||
"samples": ["155", 0],
|
||||
"vae": ["8", 0]
|
||||
},
|
||||
"class_type": "VAEDecodeTiled"
|
||||
},
|
||||
"162": {
|
||||
"inputs": {
|
||||
"b1": 1.1500000000000001,
|
||||
"b2": 1.35,
|
||||
"s1": 0.9500000000000001,
|
||||
"s2": 0.18,
|
||||
"model": ["16", 0]
|
||||
},
|
||||
"class_type": "FreeU"
|
||||
},
|
||||
"163": {
|
||||
"inputs": {
|
||||
"Value": 0.5
|
||||
},
|
||||
"class_type": "Float"
|
||||
},
|
||||
"168": {
|
||||
"inputs": {
|
||||
"image": "Layer 4 (3).png",
|
||||
"choose file to upload": "image"
|
||||
},
|
||||
"class_type": "LoadImage"
|
||||
},
|
||||
"169": {
|
||||
"inputs": {
|
||||
"image": "Layer 3 (1).png",
|
||||
"choose file to upload": "image"
|
||||
},
|
||||
"class_type": "LoadImage"
|
||||
},
|
||||
"188": {
|
||||
"inputs": {
|
||||
"image": "01285-3246154361-a photo of charddim15 person looking happy, beautiful, ((cloth)), ((full body)), ((chest)), ((far away)), ((waist up)).png",
|
||||
"choose file to upload": "image"
|
||||
},
|
||||
"class_type": "LoadImage"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
import hi_res_workflow from './hi_res_workflow.json'
|
||||
import img2img_workflow from './img2img_workflow.json'
|
||||
import animatediff_workflow from './animatediff_workflow.json'
|
||||
import lora_less_workflow from './lora_less_workflow.json'
|
||||
import { diffusion_chain } from '../entry'
|
||||
import { ComfyPrompt } from 'diffusion-chain/dist/backends/comfyui-api.mjs'
|
||||
// import { ComfyPrompt } from 'diffusion-chain/dist/backends/comfyui-api.mjs'
|
||||
// import { ComfyPrompt } from 'diffusion-chain/'
|
||||
export function getWorkflow() {}
|
||||
|
||||
interface Workflow {}
|
||||
export function getNodes(workflow: Workflow) {
|
||||
// Object.values(workflow).forEach((node) => {
|
||||
// console.log(node.class_type)
|
||||
// })
|
||||
return Object.entries(workflow)
|
||||
}
|
||||
|
||||
export enum ComfyInputType {
|
||||
TextField = 'TextField',
|
||||
TextArea = 'TextArea',
|
||||
Menu = 'Menu',
|
||||
Number = 'Number',
|
||||
Slider = 'Slider',
|
||||
BigNumber = 'BigNumber',
|
||||
TextFieldNumber = 'TextFieldNumber',
|
||||
Skip = 'Skip',
|
||||
}
|
||||
export enum ComfyNodeType {
|
||||
LoadImage = 'LoadImage',
|
||||
Normal = 'Normal',
|
||||
Skip = 'Skip',
|
||||
}
|
||||
|
||||
interface ComfyOutputImage {
|
||||
filename: string
|
||||
subfolder: string
|
||||
type: string
|
||||
}
|
||||
|
||||
export function getNodeType(node_name: any) {
|
||||
let node_type: ComfyNodeType = ComfyNodeType.Normal
|
||||
switch (node_name) {
|
||||
case 'LoadImage':
|
||||
node_type = ComfyNodeType.LoadImage
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return node_type
|
||||
}
|
||||
export function parseComfyInput(input_info: any): {
|
||||
type: ComfyInputType
|
||||
config: any
|
||||
} {
|
||||
const value = input_info[0]
|
||||
|
||||
let input_type: ComfyInputType = ComfyInputType.Skip
|
||||
let input_config
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (value === 'FLOAT') {
|
||||
input_type = ComfyInputType.Slider
|
||||
input_config = input_info[1]
|
||||
} else if (value === 'INT') {
|
||||
if (input_info[1].max > Number.MAX_SAFE_INTEGER) {
|
||||
input_type = ComfyInputType.BigNumber
|
||||
input_config = input_info[1]
|
||||
} else {
|
||||
input_type = ComfyInputType.TextFieldNumber
|
||||
input_config = input_info[1]
|
||||
}
|
||||
} else if (value === 'STRING') {
|
||||
if (input_info[1]?.multiline) {
|
||||
input_type = ComfyInputType.TextArea
|
||||
input_config = input_info[1]
|
||||
} else {
|
||||
input_type = ComfyInputType.TextField
|
||||
input_config = input_info[1]
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
input_type = ComfyInputType.Menu
|
||||
input_config = value
|
||||
}
|
||||
|
||||
return { type: input_type, config: input_config }
|
||||
}
|
||||
|
||||
export function makeHtmlInput() {}
|
||||
|
||||
export function nodeToUIConfig(
|
||||
node: { inputs: { [key: string]: any }; class_type: string },
|
||||
object_info: any
|
||||
) {
|
||||
let comfy_node_info = object_info[node.class_type]
|
||||
let node_ui_config = Object.entries(node.inputs).map(
|
||||
([name, value]: [string, any]) => {
|
||||
const first_value = comfy_node_info[name][0]
|
||||
let { type, config } = parseComfyInput(first_value)
|
||||
|
||||
return
|
||||
}
|
||||
)
|
||||
// comfy_node_info.input.required[]
|
||||
}
|
||||
|
||||
async function getHistory(comfy_server: diffusion_chain.ComfyServer) {
|
||||
while (true) {
|
||||
const res = await diffusion_chain.ComfyApi.queue(comfy_server)
|
||||
if (res.queue_pending.length || res.queue_running.length) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
} else {
|
||||
break
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
const history = await diffusion_chain.ComfyApi.history(comfy_server)
|
||||
return history
|
||||
}
|
||||
export async function postPromptAndGetBase64JsonResult(
|
||||
comfy_server: diffusion_chain.ComfyServer,
|
||||
prompt: Record<string, any>
|
||||
) {
|
||||
try {
|
||||
const res = await diffusion_chain.ComfyApi.prompt(comfy_server, {
|
||||
prompt,
|
||||
} as ComfyPrompt)
|
||||
if (res.error) {
|
||||
const readable_error = comfy_server.getReadableError(res)
|
||||
throw new Error(readable_error)
|
||||
}
|
||||
const prompt_id = res.prompt_id
|
||||
const history = await getHistory(comfy_server)
|
||||
const promptInfo = history[prompt_id]
|
||||
const store_output = await mapComfyOutputToStoreOutput(
|
||||
comfy_server,
|
||||
promptInfo.outputs
|
||||
)
|
||||
// // [4][0] for output id.
|
||||
// const fileName = promptInfo.outputs[promptInfo.prompt[4][0]].images[0].filename
|
||||
// const resultB64 = await ComfyApi.view(this, fileName);
|
||||
// resultImages.push(resultB64)
|
||||
// if (option.imageFinishCallback) {
|
||||
// try { option.imageFinishCallback(resultB64, index) } catch (e) { }
|
||||
// }
|
||||
// }
|
||||
return store_output
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
export const getFileFormat = (fileName: string): string =>
|
||||
fileName.includes('.') ? fileName.split('.').pop()! : ''
|
||||
|
||||
export async function base64UrlFromComfy(
|
||||
comfy_server: diffusion_chain.ComfyServer,
|
||||
{ filename, type, subfolder }: ComfyOutputImage
|
||||
) {
|
||||
const base64 = await diffusion_chain.ComfyApi.view(
|
||||
comfy_server,
|
||||
filename,
|
||||
type,
|
||||
subfolder
|
||||
)
|
||||
return base64Url(base64, getFileFormat(filename))
|
||||
}
|
||||
export function base64UrlFromFileName(base64: string, filename: string) {
|
||||
return base64Url(base64, getFileFormat(filename))
|
||||
}
|
||||
export function base64Url(base64: string, format: string = 'png') {
|
||||
return `data:image/${format};base64,${base64}`
|
||||
}
|
||||
export function generatePrompt(prompt: Record<string, any>) {
|
||||
prompt
|
||||
}
|
||||
export function updateOutput(output: any, output_store_obj: any) {
|
||||
// store.data.current_prompt2_output[26] = [image, image]
|
||||
output_store_obj = output
|
||||
}
|
||||
|
||||
export async function mapComfyOutputToStoreOutput(
|
||||
comfy_server: diffusion_chain.ComfyServer,
|
||||
comfy_output: Record<string, any>
|
||||
) {
|
||||
// const comfy_output: Record<string, any> = {
|
||||
// '12': {
|
||||
// images: [
|
||||
// {
|
||||
// filename: 'AA_readme_00506_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00507_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00508_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00509_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00510_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00511_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00512_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00513_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00514_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00515_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00516_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00517_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00518_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00519_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00520_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// {
|
||||
// filename: 'AA_readme_00521_.png',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// '26': {
|
||||
// images: [
|
||||
// {
|
||||
// filename: 'AA_readme_gif_00079_.gif',
|
||||
// subfolder: '',
|
||||
// type: 'output',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// }
|
||||
|
||||
const store_output: Record<string, any> = {}
|
||||
for (let key in comfy_output) {
|
||||
if (comfy_output[key].hasOwnProperty('images')) {
|
||||
let base64_url_list = await Promise.all(
|
||||
comfy_output[key].images.map(
|
||||
async (image: ComfyOutputImage) =>
|
||||
await base64UrlFromComfy(comfy_server, image)
|
||||
)
|
||||
)
|
||||
store_output[key] = base64_url_list
|
||||
}
|
||||
}
|
||||
return store_output
|
||||
}
|
||||
|
||||
interface LooseObject {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
function isSameStructure(obj1: LooseObject, obj2: LooseObject): boolean {
|
||||
// Get keys
|
||||
const keys1 = Object.keys(obj1)
|
||||
const keys2 = Object.keys(obj2)
|
||||
|
||||
// Check if both objects have the same number of keys
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if all keys in obj1 exist in obj2 and have the same structure
|
||||
for (let i = 0; i < keys1.length; i++) {
|
||||
const key = keys1[i]
|
||||
|
||||
// Check if the key exists in obj2
|
||||
if (!obj2.hasOwnProperty(key)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the value of this key is an object, check their structure recursively
|
||||
if (
|
||||
typeof obj1[key] === 'object' &&
|
||||
obj1[key] !== null &&
|
||||
typeof obj2[key] === 'object' &&
|
||||
obj2[key] !== null
|
||||
) {
|
||||
if (!isSameStructure(obj1[key], obj2[key])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all checks passed, the structures are the same
|
||||
return true
|
||||
}
|
||||
|
||||
export default {
|
||||
getNodes,
|
||||
parseComfyInput,
|
||||
getNodeType,
|
||||
base64Url,
|
||||
getFileFormat,
|
||||
base64UrlFromComfy,
|
||||
generatePrompt,
|
||||
updateOutput,
|
||||
getHistory,
|
||||
mapComfyOutputToStoreOutput,
|
||||
postPromptAndGetBase64JsonResult,
|
||||
isSameStructure,
|
||||
hi_res_workflow,
|
||||
img2img_workflow,
|
||||
animatediff_workflow,
|
||||
lora_less_workflow,
|
||||
ComfyInputType,
|
||||
ComfyNodeType,
|
||||
}
|
||||
|
|
@ -40,3 +40,6 @@ export * as api_ts from './util/ts/api'
|
|||
export * as comfyui from './comfyui/comfyui'
|
||||
export { toJS } from 'mobx'
|
||||
export { default as node_fs } from 'fs'
|
||||
export { default as comfyui_util } from './comfyui/util'
|
||||
|
||||
export * as diffusion_chain from 'diffusion-chain'
|
||||
|
|
|
|||
Loading…
Reference in New Issue