├── svelte ├── .npmrc ├── src │ ├── app.sass │ ├── lib │ │ ├── index.ts │ │ ├── multi-dim-table │ │ │ ├── score.svelte │ │ │ ├── utils.ts │ │ │ ├── table-holder.svelte │ │ │ ├── store.ts │ │ │ ├── table-cell-axis.svelte │ │ │ ├── models.ts │ │ │ ├── table-axis.svelte │ │ │ ├── table.svelte │ │ │ ├── layout.svelte │ │ │ └── table-cell-img.svelte │ │ ├── random.ts │ │ ├── icons │ │ │ └── info.svelte │ │ └── db.ts │ ├── routes │ │ ├── +layout.svelte │ │ ├── FilesTab.svelte │ │ ├── xyz_plot │ │ │ ├── +layout.ts │ │ │ └── +page.svelte │ │ ├── +layout.ts │ │ ├── Toast.svelte │ │ ├── +page.svelte │ │ ├── ModelsTab.svelte │ │ ├── MediaShow.svelte │ │ ├── NewDownload.svelte │ │ ├── DownloadHistory.svelte │ │ ├── utils.ts │ │ ├── FilesList.svelte │ │ ├── Navbar.svelte │ │ ├── CollectionsTab.svelte │ │ └── SourcesTab.svelte │ ├── app.d.ts │ ├── app.html │ └── i18n │ │ ├── index.ts │ │ ├── zh-CN.json │ │ └── en-US.json ├── .prettierrc ├── static │ └── favicon.png ├── postcss.config.js ├── .vscode │ └── settings.json ├── .gitignore ├── tailwind.config.js ├── vite.config.ts ├── svelte.config.js ├── tsconfig.json ├── README.md └── package.json ├── web ├── build │ ├── _app │ │ ├── env.js │ │ ├── version.json │ │ └── immutable │ │ │ ├── chunks │ │ │ ├── each.-gASlQSi.js │ │ │ ├── _commonjsHelpers.Cpj98o6Y.js │ │ │ ├── index.BlOrB6Vx.js │ │ │ ├── preload-helper.Dch09mLN.js │ │ │ ├── zh-CN.BEOSgdra.js │ │ │ ├── en-US.3ZgkfkIk.js │ │ │ ├── scheduler.DD_VFgMU.js │ │ │ ├── singletons.Bc_UULDX.js │ │ │ └── index.m7aY60kV.js │ │ │ ├── nodes │ │ │ ├── 2.-Y4AVwMq.js │ │ │ ├── 1.E1uHrSfC.js │ │ │ └── 0.BLschTxR.js │ │ │ └── entry │ │ │ └── app.D5ml1A7d.js │ ├── favicon.png │ ├── xyz_plot.html │ └── index.html ├── nodes │ ├── xyzPlot.js │ └── selectInputs.js └── index.js ├── requirements.txt ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ └── new-workflow-repository.md └── workflows │ └── publish.yml ├── pyproject.toml ├── CHANGELOG.md ├── nodes ├── select_inputs.py ├── dify_text_generator.py ├── load_image_by_url.py ├── upload_to_remote.py └── xyz_plot.py ├── routes ├── config.py ├── collections.py ├── sources.py ├── files.py ├── xyz_plot.py └── downloads.py ├── README_CN.md ├── __init__.py ├── data └── sources.json ├── README.md └── utils.py /svelte/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /web/build/_app/env.js: -------------------------------------------------------------------------------- 1 | export const env={} -------------------------------------------------------------------------------- /web/build/_app/version.json: -------------------------------------------------------------------------------- 1 | {"version":"1714203421307"} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm 2 | pandas 3 | numpy 4 | requests 5 | urllib3 6 | -------------------------------------------------------------------------------- /svelte/src/app.sass: -------------------------------------------------------------------------------- 1 | @tailwind base 2 | @tailwind components 3 | @tailwind utilities 4 | -------------------------------------------------------------------------------- /svelte/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "semi": true 5 | } 6 | -------------------------------------------------------------------------------- /svelte/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /web/build/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talesofai/comfyui-browser/HEAD/web/build/favicon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | __pycache__/ 3 | collections/ 4 | sources/ 5 | .DS_Store 6 | download_logs/ 7 | -------------------------------------------------------------------------------- /svelte/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talesofai/comfyui-browser/HEAD/svelte/static/favicon.png -------------------------------------------------------------------------------- /svelte/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /web/build/_app/immutable/chunks/each.-gASlQSi.js: -------------------------------------------------------------------------------- 1 | function e(n){return n?.length!==void 0?n:Array.from(n)}export{e}; 2 | -------------------------------------------------------------------------------- /svelte/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /svelte/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": [ 3 | "src/i18n" 4 | ], 5 | "i18n-ally.keystyle": "nested" 6 | } -------------------------------------------------------------------------------- /svelte/src/lib/multi-dim-table/score.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | ({score}) 7 | 8 | -------------------------------------------------------------------------------- /svelte/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /svelte/src/lib/multi-dim-table/utils.ts: -------------------------------------------------------------------------------- 1 | export function zip(arr1: T[], arr2: U[]): [T, U][] { 2 | return Array.from({ length: arr1.length }).map((_, i) => [arr1[i], arr2[i]]); 3 | } 4 | -------------------------------------------------------------------------------- /svelte/src/lib/random.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | 3 | export function fakeUsername() { 4 | const now = Date.now(); 5 | const suf = now.toString(32); 6 | return `${faker.person.lastName()}_${suf}`; 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-workflow-repository.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New workflow repository 3 | about: new workflow repository 4 | title: 'New workflow repo:' 5 | labels: workflow-repo 6 | assignees: tzwm 7 | 8 | --- 9 | 10 | My workflow repository link: 11 | Description: 12 | -------------------------------------------------------------------------------- /web/build/_app/immutable/chunks/_commonjsHelpers.Cpj98o6Y.js: -------------------------------------------------------------------------------- 1 | var o=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function l(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}export{o as c,l as g}; 2 | -------------------------------------------------------------------------------- /svelte/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("daisyui")], 8 | daisyui: { 9 | themes: ["forest"], 10 | }, 11 | } 12 | 13 | -------------------------------------------------------------------------------- /svelte/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /svelte/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | server: { 7 | proxy: { 8 | '/browser': { 9 | target: 'http://127.0.0.1:8188', 10 | }, 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /svelte/src/routes/FilesTab.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /svelte/src/routes/xyz_plot/+layout.ts: -------------------------------------------------------------------------------- 1 | import { waitLocale } from 'svelte-i18n'; 2 | 3 | /** @type {import('./$types').PageLoad} */ 4 | export async function load({ url }) { 5 | await waitLocale(); 6 | 7 | if (typeof process === 'undefined') { 8 | const path = url.searchParams.get('path'); 9 | return { 10 | path, 11 | }; 12 | } 13 | return {}; 14 | } 15 | -------------------------------------------------------------------------------- /svelte/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /svelte/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | import '../i18n/index'; 2 | import { waitLocale } from 'svelte-i18n'; 3 | 4 | export const prerender = true; 5 | 6 | /** @type {import('./$types').PageLoad} */ 7 | export async function load({ url }) { 8 | await waitLocale(); 9 | 10 | let comfyUrl = ''; 11 | if (typeof process === 'undefined') { 12 | comfyUrl = url.searchParams.get('comfyUrl') || ''; 13 | } 14 | 15 | return { 16 | comfyUrl, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /svelte/src/lib/multi-dim-table/table-holder.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#if value.type === 'img'} 11 | 12 | {:else if value.type === 'axis'} 13 | 14 | {/if} 15 | -------------------------------------------------------------------------------- /svelte/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | export default { 5 | kit: { 6 | adapter: adapter({ 7 | // default options are shown. On some platforms 8 | // these options are set automatically — see below 9 | pages: '../web/build', 10 | assets: '../web/build', 11 | fallback: undefined, 12 | precompress: false, 13 | strict: true, 14 | }), 15 | }, 16 | preprocess: vitePreprocess(), 17 | } -------------------------------------------------------------------------------- /svelte/src/lib/multi-dim-table/store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import PubSub from 'pubsub-js'; 3 | 4 | export const imageWidth = writable(50); 5 | export const comfyUrl = writable(''); 6 | 7 | export enum TableMode { 8 | Score, 9 | View, 10 | } 11 | export const mode = writable(TableMode.View); 12 | 13 | export function createRefetchStatisticPublisher() { 14 | return () => PubSub.publish('refetch-statistics'); 15 | } 16 | 17 | export function createRefetchStatisticSubscriber(fn: () => void) { 18 | return PubSub.subscribe('refetch-statistics', fn); 19 | } 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-browser" 3 | description = "This is an image/video/workflow browser and manager for ComfyUI. You could add image/video/workflow to collections and load it to ComfyUI. You will be able to use your collections everywhere." 4 | version = "1.0.0" 5 | license = "LICENSE" 6 | dependencies = ["tqdm", "pandas", "numpy", "requests", "urllib3"] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/talesofai/comfyui-browser" 10 | # Used by Comfy Registry https://comfyregistry.org 11 | 12 | [tool.comfy] 13 | PublisherId = "" 14 | DisplayName = "comfyui-browser" 15 | Icon = "" 16 | -------------------------------------------------------------------------------- /svelte/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "pyproject.toml" 9 | 10 | jobs: 11 | publish-node: 12 | name: Publish Custom Node to registry 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v4 17 | - name: Publish Custom Node 18 | uses: Comfy-Org/publish-node-action@main 19 | with: 20 | ## Add your own personal access token to your Github Repository secrets and reference it here. 21 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} -------------------------------------------------------------------------------- /svelte/src/lib/multi-dim-table/table-cell-axis.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 | {#if score} 12 | 13 | {/if} 14 | {value.value} 15 |
16 |
17 | {#each value.children as item} 18 | 19 | {/each} 20 |
21 |
22 | -------------------------------------------------------------------------------- /svelte/src/lib/multi-dim-table/models.ts: -------------------------------------------------------------------------------- 1 | export interface Payload { 2 | workflow: Workflow; 3 | annotations: Annotation[]; 4 | result: Axis[]; 5 | } 6 | 7 | export interface Workflow { 8 | url: string; 9 | } 10 | 11 | export interface Annotation { 12 | axis: string; 13 | key: string; 14 | type: string; 15 | } 16 | 17 | export interface Axis { 18 | type: 'axis'; 19 | value: string; 20 | children: AxisValue[]; 21 | } 22 | 23 | export type AxisValue = Axis | ImgResult; 24 | 25 | export interface ImgResult { 26 | type: 'img'; 27 | uuid: string; 28 | src: string; 29 | } 30 | 31 | export interface AxisScore { 32 | type: 'axis'; 33 | score: number; 34 | children?: AxisScore[]; 35 | } 36 | -------------------------------------------------------------------------------- /svelte/src/routes/Toast.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | {#if showToast} 20 |
21 |
22 | {toastText} 23 |
24 |
25 | {/if} 26 | -------------------------------------------------------------------------------- /svelte/src/lib/icons/info.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 16 | -------------------------------------------------------------------------------- /web/build/_app/immutable/nodes/2.-Y4AVwMq.js: -------------------------------------------------------------------------------- 1 | import{w as r}from"../chunks/runtime.CA0Siqq1.js";import{s as l,c as i,u,g as f,d as c}from"../chunks/scheduler.DD_VFgMU.js";import{S as _,i as p,d as m,t as d}from"../chunks/index.m7aY60kV.js";async function g({url:o}){return await r(),typeof process>"u"?{path:o.searchParams.get("path")}:{}}const v=Object.freeze(Object.defineProperty({__proto__:null,load:g},Symbol.toStringTag,{value:"Module"}));function $(o){let s;const n=o[1].default,e=i(n,o,o[0],null);return{c(){e&&e.c()},l(t){e&&e.l(t)},m(t,a){e&&e.m(t,a),s=!0},p(t,[a]){e&&e.p&&(!s||a&1)&&u(e,n,t,t[0],s?c(n,t[0],a,null):f(t[0]),null)},i(t){s||(m(e,t),s=!0)},o(t){d(e,t),s=!1},d(t){e&&e.d(t)}}}function h(o,s,n){let{$$slots:e={},$$scope:t}=s;return o.$$set=a=>{"$$scope"in a&&n(0,t=a.$$scope)},[t,e]}class w extends _{constructor(s){super(),p(this,s,h,$,l,{})}}export{w as component,v as universal}; 2 | -------------------------------------------------------------------------------- /web/build/_app/immutable/chunks/index.BlOrB6Vx.js: -------------------------------------------------------------------------------- 1 | import{n as f,l as w,r as m,s as q,k as x}from"./scheduler.DD_VFgMU.js";const a=[];function z(e,i){return{subscribe:A(e,i).subscribe}}function A(e,i=f){let r;const n=new Set;function u(t){if(q(e,t)&&(e=t,r)){const o=!a.length;for(const s of n)s[1](),a.push(s,e);if(o){for(let s=0;s{n.delete(s),n.size===0&&r&&(r(),r=null)}}return{set:u,update:l,subscribe:b}}function B(e,i,r){const n=!Array.isArray(e),u=n?[e]:e;if(!u.every(Boolean))throw new Error("derived() expects stores as input, got a falsy value");const l=i.length<2;return z(r,(b,t)=>{let o=!1;const s=[];let d=0,p=f;const h=()=>{if(d)return;p();const c=i(n?s[0]:s,b,t);l?b(c):p=x(c)?c:f},y=u.map((c,g)=>w(c,_=>{s[g]=_,d&=~(1<{d|=1< import('./en-US.json')); 5 | register('zh-CN', () => import('./zh-CN.json')); 6 | 7 | const langKey = 'lang'; 8 | const defaultLocale = 'en-US'; 9 | 10 | let initialLocale: string | null = browser ? window.navigator.language : defaultLocale; 11 | if (getLocaleFromNavigator()) { 12 | initialLocale = getLocaleFromNavigator(); 13 | } 14 | if (browser && localStorage.getItem(langKey)) { 15 | initialLocale = localStorage.getItem(langKey); 16 | } 17 | 18 | init({ 19 | fallbackLocale: defaultLocale, 20 | initialLocale: initialLocale, 21 | handleMissingMessage: (input: any) => input.id.split('.').pop(), 22 | }); 23 | 24 | locale.subscribe((value: any) => { 25 | //console.log('changed lang to ', value); 26 | 27 | if (browser) { 28 | localStorage.setItem(langKey, value); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /web/build/_app/immutable/nodes/1.E1uHrSfC.js: -------------------------------------------------------------------------------- 1 | import{s as E,n as b,e as x}from"../chunks/scheduler.DD_VFgMU.js";import{S,i as j,g as _,m as f,s as q,h as d,j as g,n as h,f as p,c as y,a as l,x as v,o as $}from"../chunks/index.m7aY60kV.js";import{d as C}from"../chunks/singletons.Bc_UULDX.js";const H=()=>{const s=C;return{page:{subscribe:s.page.subscribe},navigating:{subscribe:s.navigating.subscribe},updated:s.updated}},P={subscribe(s){return H().page.subscribe(s)}};function k(s){let t,r=s[0].status+"",o,n,i,c=s[0].error?.message+"",u;return{c(){t=_("h1"),o=f(r),n=q(),i=_("p"),u=f(c)},l(e){t=d(e,"H1",{});var a=g(t);o=h(a,r),a.forEach(p),n=y(e),i=d(e,"P",{});var m=g(i);u=h(m,c),m.forEach(p)},m(e,a){l(e,t,a),v(t,o),l(e,n,a),l(e,i,a),v(i,u)},p(e,[a]){a&1&&r!==(r=e[0].status+"")&&$(o,r),a&1&&c!==(c=e[0].error?.message+"")&&$(u,c)},i:b,o:b,d(e){e&&(p(t),p(n),p(i))}}}function w(s,t,r){let o;return x(s,P,n=>r(0,o=n)),[o]}let D=class extends S{constructor(t){super(),j(this,t,w,k,E,{})}};export{D as component}; 2 | -------------------------------------------------------------------------------- /web/nodes/xyzPlot.js: -------------------------------------------------------------------------------- 1 | import { app } from "../../../scripts/app.js"; 2 | 3 | const xyzPlotNodeType = 'XyzPlot //Browser'; 4 | 5 | app.registerExtension({ 6 | name: "Browser.Nodes.XyzPlot", 7 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 8 | if (nodeData.name != xyzPlotNodeType) { 9 | return; 10 | } 11 | const orig = nodeType.prototype.onExecuted; 12 | nodeType.prototype.onExecuted = function(message) { 13 | const ret = orig?.apply(this, arguments); 14 | 15 | const resultPath = message.result_path[0]; 16 | if (!resultPath) { 17 | return ret; 18 | } 19 | 20 | let button = this.widgets.find(w => w.type === 'button'); 21 | const callback = () => { window.open(window.location.origin + resultPath, "_blank"); }; 22 | if (!button) { 23 | this.addWidget("button", 'Open the result', '', callback); 24 | } else { 25 | button.callback = callback; 26 | } 27 | 28 | return ret; 29 | }; 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /svelte/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 26 | {#if activeTab === 'collections'} 27 | 28 | {:else if activeTab === 'sources'} 29 | 30 | {:else if activeTab === 'models'} 31 | 32 | {:else} 33 | 34 | {/if} 35 | -------------------------------------------------------------------------------- /web/build/_app/immutable/chunks/preload-helper.Dch09mLN.js: -------------------------------------------------------------------------------- 1 | const v="modulepreload",y=function(a,i){return new URL(a,i).href},d={},p=function(i,l,u){let f=Promise.resolve();if(l&&l.length>0){const r=document.getElementsByTagName("link"),n=document.querySelector("meta[property=csp-nonce]"),h=n?.nonce||n?.getAttribute("nonce");f=Promise.all(l.map(e=>{if(e=y(e,u),e in d)return;d[e]=!0;const s=e.endsWith(".css"),m=s?'[rel="stylesheet"]':"";if(!!u)for(let o=r.length-1;o>=0;o--){const c=r[o];if(c.href===e&&(!s||c.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${e}"]${m}`))return;const t=document.createElement("link");if(t.rel=s?"stylesheet":v,s||(t.as="script",t.crossOrigin=""),t.href=e,h&&t.setAttribute("nonce",h),document.head.appendChild(t),s)return new Promise((o,c)=>{t.addEventListener("load",o),t.addEventListener("error",()=>c(new Error(`Unable to preload CSS for ${e}`)))})}))}return f.then(()=>i()).catch(r=>{const n=new Event("vite:preloadError",{cancelable:!0});if(n.payload=r,window.dispatchEvent(n),!n.defaultPrevented)throw r})};export{p as _}; 2 | -------------------------------------------------------------------------------- /svelte/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /web/build/_app/immutable/chunks/zh-CN.BEOSgdra.js: -------------------------------------------------------------------------------- 1 | const e={outputs:"输出",saves:"收藏",sources:"订阅",models:"模型"},t={emptyList:"暂无数据",loading:"加载中...",rootDir:"根目录",btn:{load:"加载",save:"收藏",delete:"删除",add:"新增",refresh:"刷新"}},o={searchInput:{placeholder:"根据文件名和备忘搜索"},btn:{save:"保存",sync:"同步",syncing:"同步中"},syncInput:{placeholder:"Git 地址,比如 git@github.com:tzwm/comfyui-workflows.git"},collection:{memoPlaceholder:"写个备忘,方便搜索"},toast:{synced:"同步完成",syncFailed:"同步失败。请在控制台查看详情。",configUpdated:"保存成功",configUpdatedFailed:"保存失败。请在控制台查看详情。",deleteConfirm:"确认删除这个文件:",deleteSuccess:"删除成功:",deleteFailed:"删除失败。请在控制台查看详情。",updated:"更新成功",UpdatedFailed:"更新失败。请在控制台查看详情。","Invalid filename":"不是个有效文件名"}},s={searchInput:{placeholder:"根据文件名过滤"},"Added to Saves":"收藏成功","Failed to add to Saves":"收藏失败。请在控制台查看详情。","You want to delete this file?":"确认删除这个文件?","Deleted the file":"删除成功:","Failed to delete the file":"删除失败。请在控制台查看详情。"},d={"Auto Refresh":"自动刷新"},l={"You want to delete this source":"确认删除这个订阅吗?","Deleted this source":"删除成功","Failed to delete this source":"删除失败。请在控制台查看详情。","Add new source":"订阅新的源",Subscribe:"订阅","Subscribing...":"订阅中"},a={navbar:e,common:t,collectionsTab:o,filesList:s,downloadHistory:d,sourcesTab:l};export{o as collectionsTab,t as common,a as default,d as downloadHistory,s as filesList,e as navbar,l as sourcesTab}; 2 | -------------------------------------------------------------------------------- /svelte/src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import Dexie, { type Table } from 'dexie'; 2 | export interface User { 3 | uuid: string; 4 | name: string; 5 | ctime: number; 6 | mtime: number; 7 | } 8 | 9 | export interface Score { 10 | uuid: string; 11 | score: number; 12 | link?: string; 13 | ctime: number; 14 | mtime: number; 15 | } 16 | 17 | export class ScoreDB extends Dexie { 18 | user!: Table; 19 | scoreboard!: Table; 20 | 21 | constructor() { 22 | super('scores'); 23 | this.version(1).stores({ 24 | user: 'uuid, name, ctime, mtime', 25 | scoreboard: 'uuid, score, link, ctime, mtime', 26 | }); 27 | } 28 | } 29 | 30 | export const db = new ScoreDB(); 31 | 32 | export function getUser() { 33 | return db.user.toArray().then((d) => (d.length > 0 ? d[0] : null)); 34 | } 35 | 36 | export function getScore(uuid: string) { 37 | return db.scoreboard 38 | .where('uuid') 39 | .equals(uuid) 40 | .toArray() 41 | .then((d) => (d.length > 0 ? d[0] : null)); 42 | } 43 | 44 | export function updateScore( 45 | uuid: string, 46 | value: Partial> 47 | ) { 48 | return db.scoreboard.where('uuid').equals(uuid).modify({ 49 | link: value.link, 50 | score: value.score, 51 | mtime: Date.now(), 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /svelte/src/lib/multi-dim-table/table-axis.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | {#if score} 14 | 15 | {/if} 16 | {axis.value} 17 | 18 | {#if score && score.children} 19 | {#each zip(axis.children, score.children) as [dim2, score2]} 20 | {#if dim2.type === 'axis' && score2.children} 21 | 22 | {#each zip(dim2.children, score2.children) as [dim3, score3]} 23 | 24 | {/each} 25 | 26 | {/if} 27 | {/each} 28 | {:else} 29 | {#each axis.children as dim2} 30 | {#if dim2.type === 'axis'} 31 | 32 | {#each dim2.children as dim3} 33 | 34 | {/each} 35 | 36 | {/if} 37 | {/each} 38 | {/if} 39 | 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | ## v1.8.1 - 2024-03-22 4 | 5 | - Update xyz_plot to support score mode 6 | 7 | ## v1.8.0 - 2024-03-19 8 | 9 | - Update xyz_plot to support Z axis 10 | 11 | ## v1.7.0 - 2024-01-14 12 | 13 | - Add xyz_plot and select_inputs nodes 14 | 15 | ## v1.6.1 - 2023-12-31 16 | 17 | - Fix some bugs about CollectionsTab 18 | 19 | ## v1.6.0 - 2023-12-30 20 | 21 | - Add i18n support and zh-CN language 22 | 23 | ## v1.5.3 - 2023-12-23 24 | 25 | - Merge the same name JSON and media file in the same directory 26 | 27 | ## v1.5.2 - 2023-12-20 28 | 29 | - Support resize the modal 30 | - Remember the last Tab select 31 | 32 | ## v1.5.1 - 2023-12-20 33 | 34 | - Add Side/Center toggle 35 | - Update styles for responsive 36 | 37 | ## v1.5.0 - 2023-12-15 38 | 39 | - Update styles 40 | - Add recommend sources 41 | 42 | ## v1.4.0 - 2023-12-13 43 | 44 | - Add 'Sources' tab to subscribe external workflow git repository 45 | 46 | ## v1.3.0 - 2023-12-11 47 | 48 | - Support subfolder for output and collection 49 | 50 | ## v1.2.0 - 2023-12-10 51 | 52 | - Can collect current graph to Collections as a workflow.json 53 | 54 | ## v1.1.0 - 2023-12-09 55 | 56 | - Can sync collections to remote git repository 57 | 58 | ## v1.0.0 - 2023-12-08 59 | 60 | - Add image/video/workflow Browser for output folder 61 | - Add Collections for image/video/workflow 62 | -------------------------------------------------------------------------------- /svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "comfyui-browser", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "extract": "svelte-i18n extract './src/**/*.svelte' ./src/i18n/zh-CN.json && svelte-i18n extract './src/**/*.svelte' ./src/i18n/en-US.json" 12 | }, 13 | "devDependencies": { 14 | "@faker-js/faker": "^8.4.1", 15 | "@sveltejs/adapter-auto": "^3.0.1", 16 | "@sveltejs/adapter-static": "^3.0.1", 17 | "@sveltejs/kit": "^2.0.4", 18 | "@sveltejs/vite-plugin-svelte": "^3.0.1", 19 | "@types/node": "^20.11.28", 20 | "@types/pubsub-js": "^1.8.6", 21 | "@types/uuid": "^9.0.8", 22 | "autoprefixer": "^10.4.16", 23 | "daisyui": "^4.4.22", 24 | "dexie": "4.0.1-beta.14", 25 | "postcss": "^8.4.32", 26 | "pubsub-js": "^1.9.4", 27 | "sass": "^1.72.0", 28 | "svelte": "^4.2.8", 29 | "svelte-check": "^3.6.2", 30 | "tailwindcss": "^3.4.0", 31 | "tslib": "^2.6.2", 32 | "typescript": "^5.3.3", 33 | "uuid": "^9.0.1", 34 | "vite": "^5.0.10" 35 | }, 36 | "type": "module", 37 | "dependencies": { 38 | "dayjs": "^1.11.10", 39 | "svelte-i18n": "^4.0.0" 40 | } 41 | } -------------------------------------------------------------------------------- /web/build/_app/immutable/nodes/0.BLschTxR.js: -------------------------------------------------------------------------------- 1 | import{_ as c}from"../chunks/preload-helper.Dch09mLN.js";import{r as u,g as n,i as _,$ as f,w as m}from"../chunks/runtime.CA0Siqq1.js";import{s as p,c as g,u as d,g as $,d as L}from"../chunks/scheduler.DD_VFgMU.js";import{S as y,i as S,d as v,t as b}from"../chunks/index.m7aY60kV.js";u("en-US",()=>c(()=>import("../chunks/en-US.3ZgkfkIk.js"),[],import.meta.url));u("zh-CN",()=>c(()=>import("../chunks/zh-CN.BEOSgdra.js"),[],import.meta.url));const r="lang",h="en-US";let i=window.navigator.language;n()&&(i=n());localStorage.getItem(r)&&(i=localStorage.getItem(r));_({fallbackLocale:h,initialLocale:i,handleMissingMessage:o=>o.id.split(".").pop()});f.subscribe(o=>{localStorage.setItem(r,o)});const w=!0;async function I({url:o}){await m();let a="";return typeof process>"u"&&(a=o.searchParams.get("comfyUrl")||""),{comfyUrl:a}}const j=Object.freeze(Object.defineProperty({__proto__:null,load:I,prerender:w},Symbol.toStringTag,{value:"Module"}));function P(o){let a;const s=o[1].default,t=g(s,o,o[0],null);return{c(){t&&t.c()},l(e){t&&t.l(e)},m(e,l){t&&t.m(e,l),a=!0},p(e,[l]){t&&t.p&&(!a||l&1)&&d(t,s,e,e[0],a?L(s,e[0],l,null):$(e[0]),null)},i(e){a||(v(t,e),a=!0)},o(e){b(t,e),a=!1},d(e){t&&t.d(e)}}}function E(o,a,s){let{$$slots:t={},$$scope:e}=a;return o.$$set=l=>{"$$scope"in l&&s(0,e=l.$$scope)},[e,t]}class z extends y{constructor(a){super(),S(this,a,E,P,p,{})}}export{z as component,j as universal}; 2 | -------------------------------------------------------------------------------- /nodes/select_inputs.py: -------------------------------------------------------------------------------- 1 | class SelectInputs: 2 | def __init__(self): 3 | pass 4 | 5 | @classmethod 6 | def INPUT_TYPES(s): 7 | return { 8 | "required": { 9 | "input_1": [["none"], {}], 10 | "input_2": [["none"], {}], 11 | "input_3": [["none"], {}], 12 | "input_4": [["none"], {}], 13 | "preview": ["STRING", {"multiline": True}], 14 | } 15 | } 16 | 17 | @classmethod 18 | def VALIDATE_INPUTS(s, input_1, input_2, input_3, input_4, preview): 19 | return True 20 | 21 | # { 22 | # "node_id": 4, 23 | # "node_title": "CheckpointLoaderSimple", 24 | # "widget_name": "ckpt_name", 25 | # } 26 | RETURN_TYPES = ("INPUT", "INPUT", "INPUT", "INPUT",) 27 | RETURN_NAMES = ("input_1", "input_2", "input_3", "input_4",) 28 | 29 | FUNCTION = "run" 30 | 31 | OUTPUT_NODE = True 32 | 33 | CATEGORY = "Browser" 34 | 35 | 36 | def run(self, input_1, input_2, input_3, input_4, preview): 37 | ret = () 38 | for input in [input_1, input_2, input_3, input_4]: 39 | node_id, node_title, widget_name = input.split("::") 40 | ret = ret + ({ 41 | "node_id": node_id[1:], 42 | "node_title": node_title, 43 | "widget_name": widget_name, 44 | },) 45 | 46 | return ret 47 | -------------------------------------------------------------------------------- /svelte/src/routes/ModelsTab.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 20 |
21 | {#if activeTab === 'downloadHistory'} 22 | 23 | {:else} 24 | onClickTab('downloadHistory')} 27 | /> 28 | {/if} 29 |
30 | 31 |
32 | 50 |
51 |
52 | -------------------------------------------------------------------------------- /routes/config.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | import json 3 | 4 | from ..utils import get_config, git_remote_name, run_cmd, git_init, \ 5 | collections_path, config_path 6 | 7 | async def api_get_browser_config(_): 8 | config = get_config() 9 | 10 | return web.json_response(config) 11 | 12 | # git_repo 13 | async def api_update_browser_config(request): 14 | json_data = await request.json() 15 | config = get_config() 16 | git_repo = json_data.get('git_repo', config.get('git_repo')) 17 | 18 | git_init() 19 | 20 | if git_repo == '': 21 | ret = run_cmd(f'git remote remove {git_remote_name}', collections_path()) 22 | if not ret.returncode == 0: 23 | return web.json_response( 24 | { 'message': ret.stderr }, 25 | status=500, 26 | ) 27 | 28 | set_config({ 'git_repo': git_repo }) 29 | return web.Response(status=200) 30 | 31 | ret = git_set_remote_url(git_repo) 32 | if not ret.returncode == 0: 33 | return web.json_response( 34 | { 'message': ret.stderr }, 35 | status=500, 36 | ) 37 | 38 | set_config({ 'git_repo': git_repo }) 39 | return web.Response(status=200) 40 | 41 | def set_config(config): 42 | with open(config_path, 'w', encoding='utf-8') as f: 43 | json.dump(config, f) 44 | 45 | def git_set_remote_url(remote_url, run_path = collections_path()): 46 | ret = run_cmd('git remote', run_path) 47 | 48 | if git_remote_name in ret.stdout.split('\n'): 49 | return run_cmd(f'git remote set-url {git_remote_name} {remote_url}', run_path) 50 | else: 51 | return run_cmd(f'git remote add {git_remote_name} {remote_url}', run_path) 52 | -------------------------------------------------------------------------------- /web/build/_app/immutable/chunks/en-US.3ZgkfkIk.js: -------------------------------------------------------------------------------- 1 | const e={outputs:"Outputs",saves:"Saves",sources:"Sources",models:"Models"},t={emptyList:"No data",loading:"Loading...",rootDir:"Root",btn:{load:"Load",save:"Save",delete:"Delete",add:"Add",refresh:"Refresh"}},o={searchInput:{placeholder:"Filter by filename or memos"},btn:{save:"Save",sync:"Sync",syncing:"Syncing"},syncInput:{placeholder:"Git URL likes 'git@github.com:tzwm/comfyui-workflows.git'"},collection:{memoPlaceholder:"write some memos..."},toast:{synced:"Synced",syncFailed:"Failed to sync. Please check the ComfyUI server.",configUpdated:"Updated config",configUpdatedFailed:"Failed to update config. Please check the ComfyUI server.",deleteConfirm:"You want to delete this file: ",deleteSuccess:"Deleted the file: ",deleteFailed:"Failed to delete the file. Please check the ComfyUI server.",updated:"Updated",updatedFailed:"Failed to update. Please check the ComfyUI server.","Invalid filename":"Invalid filename"}},s={searchInput:{placeholder:"Filter by filename"},"Added to Saves":"Added to Saves","Failed to add to Saves":"Failed to add to Saves. Please check the ComfyUI server.","You want to delete this file?":"You want to delete this file?","Deleted the file":"Deleted the file ","Failed to delete the file":"Failed to delete the file. Please check the ComfyUI server."},d={"Auto Refresh":"Auto Refresh"},l={"You want to delete this source":"You want to delete this source? ","Deleted this source":"Deleted this source.","Failed to delete this source":"Failed to delete this source.","Add new source":"Add new source",Subscribe:"Subscribe","Subscribing...":"Subscribing..."},a={navbar:e,common:t,collectionsTab:o,filesList:s,downloadHistory:d,sourcesTab:l};export{o as collectionsTab,t as common,a as default,d as downloadHistory,s as filesList,e as navbar,l as sourcesTab}; 2 | -------------------------------------------------------------------------------- /nodes/dify_text_generator.py: -------------------------------------------------------------------------------- 1 | import json 2 | from ..utils import http_client 3 | 4 | class DifyTextGenerator: 5 | CATEGORY = "Browser" 6 | 7 | RETURN_TYPES = ("STRING", ) 8 | 9 | FUNCTION = "run" 10 | 11 | OUTPUT_NODE = True 12 | 13 | @classmethod 14 | def INPUT_TYPES(s): 15 | return { 16 | "required": { 17 | "dify_api_endpoint": ["STRING", {}], 18 | "api_key": ["STRING", {}], 19 | }, 20 | "optional": { 21 | "query": ["STRING", {"multiline": True, "placeholder": "Input as the Query field."}], 22 | "inputs_json_str": ["STRING", {"multiline": True, "placeholder": "JSON format. It will overwrite the query field above."}], 23 | }, 24 | } 25 | 26 | def run(self, dify_api_endpoint, api_key, query, inputs_json_str=None): 27 | # for some special network environments like AutoDL 28 | proxies = {"http": "", "https": ""} 29 | header = { 30 | "Content-Type": "application/json", 31 | "Authorization": f"Bearer {api_key}", 32 | } 33 | data = { 34 | "user": "comfyui-browser", 35 | "response_mode": "blocking", 36 | "inputs" : { "query": query }, 37 | } 38 | if inputs_json_str and len(inputs_json_str.strip()) > 0: 39 | # something weird, I have to add '{' and '}' manually 40 | data["inputs"] = json.loads("{" + inputs_json_str + "}") 41 | 42 | r = http_client().post( 43 | dify_api_endpoint, 44 | headers=header, 45 | data=json.dumps(data), 46 | proxies=proxies 47 | ) 48 | 49 | if not r.ok: 50 | raise Exception(f"Request Dify Error: {r.text}") 51 | 52 | content = json.loads(r.content) 53 | return (content["answer"], ) 54 | -------------------------------------------------------------------------------- /svelte/src/i18n/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "navbar": { 3 | "outputs": "输出", 4 | "saves": "收藏", 5 | "sources": "订阅", 6 | "models": "模型" 7 | }, 8 | "common": { 9 | "emptyList": "暂无数据", 10 | "loading": "加载中...", 11 | "rootDir": "根目录", 12 | "btn": { 13 | "load": "加载", 14 | "save": "收藏", 15 | "delete": "删除", 16 | "add": "新增", 17 | "refresh": "刷新" 18 | } 19 | }, 20 | "collectionsTab": { 21 | "searchInput": { 22 | "placeholder": "根据文件名和备忘搜索" 23 | }, 24 | "btn": { 25 | "save": "保存", 26 | "sync": "同步", 27 | "syncing": "同步中" 28 | }, 29 | "syncInput": { 30 | "placeholder": "Git 地址,比如 git@github.com:tzwm/comfyui-workflows.git" 31 | }, 32 | "collection": { 33 | "memoPlaceholder": "写个备忘,方便搜索" 34 | }, 35 | "toast": { 36 | "synced": "同步完成", 37 | "syncFailed": "同步失败。请在控制台查看详情。", 38 | "configUpdated": "保存成功", 39 | "configUpdatedFailed": "保存失败。请在控制台查看详情。", 40 | "deleteConfirm": "确认删除这个文件:", 41 | "deleteSuccess": "删除成功:", 42 | "deleteFailed": "删除失败。请在控制台查看详情。", 43 | "updated": "更新成功", 44 | "UpdatedFailed": "更新失败。请在控制台查看详情。", 45 | "Invalid filename": "不是个有效文件名" 46 | } 47 | }, 48 | "filesList": { 49 | "searchInput": { 50 | "placeholder": "根据文件名过滤" 51 | }, 52 | "Added to Saves": "收藏成功", 53 | "Failed to add to Saves": "收藏失败。请在控制台查看详情。", 54 | "You want to delete this file?": "确认删除这个文件?", 55 | "Deleted the file": "删除成功:", 56 | "Failed to delete the file": "删除失败。请在控制台查看详情。" 57 | }, 58 | "downloadHistory": { 59 | "Auto Refresh": "自动刷新" 60 | }, 61 | "sourcesTab": { 62 | "You want to delete this source": "确认删除这个订阅吗?", 63 | "Deleted this source": "删除成功", 64 | "Failed to delete this source": "删除失败。请在控制台查看详情。", 65 | "Add new source": "订阅新的源", 66 | "Subscribe": "订阅", 67 | "Subscribing...": "订阅中" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /svelte/src/routes/MediaShow.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 59 | -------------------------------------------------------------------------------- /web/build/xyz_plot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
Invalid Path 25 | 26 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /nodes/load_image_by_url.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | import io 4 | from PIL import Image, ImageSequence, ImageOps 5 | import numpy as np 6 | import torch 7 | from ..utils import http_client 8 | 9 | 10 | import folder_paths 11 | 12 | class LoadImageByUrl: 13 | CATEGORY = "Browser" 14 | 15 | RETURN_TYPES = ("IMAGE", ) 16 | RETURN_NAMES = ("IMAGE", ) 17 | 18 | FUNCTION = "run" 19 | 20 | @classmethod 21 | def INPUT_TYPES(s): 22 | return { 23 | "required": { 24 | "url": ["STRING", {}], 25 | }, 26 | "optional": { 27 | "cache": ["BOOLEAN", {"default": True}], 28 | }, 29 | } 30 | 31 | 32 | def __init__(self): 33 | self.url = "" 34 | 35 | def filename(self): 36 | return hashlib.md5(self.url.encode()).hexdigest()[:48] + '.jpg' 37 | 38 | def download_by_url(self): 39 | input_dir = folder_paths.get_input_directory() 40 | res = http_client().get(self.url) 41 | if res.status_code == 200: 42 | download_path = os.path.join(input_dir, self.filename()) 43 | with open(download_path, 'wb') as file: 44 | file.write(res.content) 45 | return res.content 46 | else: 47 | raise ValueError(f"Failed to load image from {self.url}: {res.status_code} {res.text}") 48 | 49 | def run(self, url, cache=True): 50 | self.url = url 51 | input_dir = folder_paths.get_input_directory() 52 | image_path = os.path.join(input_dir, self.filename()) 53 | if cache == False or not os.path.isfile(image_path): 54 | img = Image.open(io.BytesIO(self.download_by_url())) 55 | else: 56 | img = Image.open(image_path) 57 | output_images = [] 58 | for i in ImageSequence.Iterator(img): 59 | i = ImageOps.exif_transpose(i) 60 | image = i.convert("RGB") 61 | image = np.array(image).astype(np.float32) / 255.0 62 | image = torch.from_numpy(image)[None,] 63 | output_images.append(image) 64 | 65 | if len(output_images) > 1: 66 | output_image = torch.cat(output_images, dim=0) 67 | else: 68 | output_image = output_images[0] 69 | 70 | return (output_image, ) 71 | -------------------------------------------------------------------------------- /web/build/_app/immutable/chunks/scheduler.DD_VFgMU.js: -------------------------------------------------------------------------------- 1 | function x(){}function w(t,n){for(const e in n)t[e]=n[e];return t}function j(t){return t()}function A(){return Object.create(null)}function E(t){t.forEach(j)}function D(t){return typeof t=="function"}function F(t,n){return t!=t?n==n:t!==n||t&&typeof t=="object"||typeof t=="function"}let i;function P(t,n){return t===n?!0:(i||(i=document.createElement("a")),i.href=n,t===i.href)}function S(t){return Object.keys(t).length===0}function q(t,...n){if(t==null){for(const r of n)r(void 0);return x}const e=t.subscribe(...n);return e.unsubscribe?()=>e.unsubscribe():e}function U(t,n,e){t.$$.on_destroy.push(q(n,e))}function B(t,n,e,r){if(t){const o=m(t,n,e,r);return t[0](o)}}function m(t,n,e,r){return t[1]&&r?w(e.ctx.slice(),t[1](r(n))):e.ctx}function C(t,n,e,r){if(t[2]&&r){const o=t[2](r(e));if(n.dirty===void 0)return o;if(typeof o=="object"){const l=[],_=Math.max(n.dirty.length,o.length);for(let s=0;s<_;s+=1)l[s]=n.dirty[s]|o[s];return l}return n.dirty|o}return n.dirty}function G(t,n,e,r,o,l){if(o){const _=m(n,e,r,l);t.p(_,o)}}function H(t){if(t.ctx.length>32){const n=[],e=t.ctx.length/32;for(let r=0;rt.indexOf(r)===-1?n.push(r):e.push(r)),e.forEach(r=>r()),u=n}export{v as A,J as a,y as b,B as c,C as d,U as e,P as f,H as g,N as h,K as i,O as j,D as k,q as l,A as m,x as n,I as o,z as p,S as q,E as r,F as s,L as t,G as u,Q as v,f as w,h as x,j as y,a as z}; 2 | -------------------------------------------------------------------------------- /svelte/src/lib/multi-dim-table/table.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 |
34 | 35 | 36 | 37 | 38 | {#if headScore} 39 | {#each zip(payload.result[0].children, headScore) as [axis, score]} 40 | {#if axis.type === 'axis'} 41 | 46 | {/if} 47 | {/each} 48 | {:else} 49 | {#each payload.result[0].children as axis} 50 | {#if axis.type === 'axis'} 51 | 54 | {/if} 55 | {/each} 56 | {/if} 57 | 58 | 59 | 60 | {#if scores} 61 | {#each zip(payload.result, scores) as [axis, score]} 62 | 63 | {/each} 64 | {:else} 65 | {#each payload.result as axis} 66 | 67 | {/each} 68 | {/if} 69 | 70 |
42 |

43 | {axis.value} 44 |

45 |
52 |

{axis.value}

53 |
71 |
72 | -------------------------------------------------------------------------------- /web/nodes/selectInputs.js: -------------------------------------------------------------------------------- 1 | import { app } from "../../../scripts/app.js"; 2 | 3 | const selectInputsNodeType = 'SelectInputs //Browser'; 4 | const SPLITTER = '::'; 5 | 6 | function getGraphInputs(graph) { 7 | let inputs = []; 8 | graph._nodes?.forEach(n => { 9 | n.widgets?.forEach(w => { 10 | inputs.push([`#${n.id}`, n.title, w.name].join(SPLITTER)); 11 | }); 12 | }); 13 | 14 | return inputs; 15 | } 16 | 17 | function refreshPreview(node) { 18 | let values = []; 19 | node.widgets.forEach(w => { 20 | if (w.type === 'combo' && w.name.startsWith('input_')) { 21 | const v = w.value.split(SPLITTER); 22 | values.push({ 23 | node_id: v[0].substring(1), 24 | node_title: v[1], 25 | widget_name: v[2], 26 | }); 27 | } 28 | }); 29 | const preview = node.widgets.find(w => w.name === 'preview'); 30 | preview.value = JSON.stringify(values); 31 | } 32 | 33 | function refreshInputs(node, app) { 34 | const inputs = getGraphInputs(app.graph); 35 | 36 | let index = -1; 37 | for (const w of node.widgets) { 38 | index += 1; 39 | if (w.type != 'combo') { 40 | return; 41 | } 42 | 43 | w.options.values = inputs; 44 | if (node.widgets_values) { 45 | w.value = node.widgets_values[index] 46 | } else { 47 | w.value = inputs[0]; 48 | } 49 | } 50 | const size = node.computeSize(); 51 | node.setSize([size[0] * 1.5, size[1]]); 52 | 53 | refreshPreview(node); 54 | } 55 | 56 | app.registerExtension({ 57 | name: "Browser.Nodes.SelectInputs", 58 | nodeCreated(node, app) { 59 | if (node.constructor.type != selectInputsNodeType) { 60 | return; 61 | } 62 | 63 | node.widgets.forEach(w => { 64 | if (w.name === 'preview') { 65 | if (w.element) { 66 | w.element.disabled = true; 67 | } 68 | } 69 | if (w.type === 'combo' && w.name.startsWith('input_')) { 70 | const oriCallback = w.callback; 71 | w.callback = () => { 72 | oriCallback?.apply(arguments); 73 | refreshPreview(node) 74 | }; 75 | } 76 | }); 77 | node.addWidget( 78 | "button", 'Refresh', '', 79 | () => refreshInputs(node, app) 80 | ); 81 | refreshInputs(node, app); 82 | }, 83 | loadedGraphNode(node, app) { 84 | if (node.type != selectInputsNodeType) { 85 | return; 86 | } 87 | 88 | refreshInputs(node, app); 89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /svelte/src/i18n/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "navbar": { 3 | "outputs": "Outputs", 4 | "saves": "Saves", 5 | "sources": "Sources", 6 | "models": "Models" 7 | }, 8 | "common": { 9 | "emptyList": "No data", 10 | "loading": "Loading...", 11 | "rootDir": "Root", 12 | "btn": { 13 | "load": "Load", 14 | "save": "Save", 15 | "delete": "Delete", 16 | "add": "Add", 17 | "refresh": "Refresh" 18 | } 19 | }, 20 | "collectionsTab": { 21 | "searchInput": { 22 | "placeholder": "Filter by filename or memos" 23 | }, 24 | "btn": { 25 | "save": "Save", 26 | "sync": "Sync", 27 | "syncing": "Syncing" 28 | }, 29 | "syncInput": { 30 | "placeholder": "Git URL likes 'git@github.com:tzwm/comfyui-workflows.git'" 31 | }, 32 | "collection": { 33 | "memoPlaceholder": "write some memos..." 34 | }, 35 | "toast": { 36 | "synced": "Synced", 37 | "syncFailed": "Failed to sync. Please check the ComfyUI server.", 38 | "configUpdated": "Updated config", 39 | "configUpdatedFailed": "Failed to update config. Please check the ComfyUI server.", 40 | "deleteConfirm": "You want to delete this file: ", 41 | "deleteSuccess": "Deleted the file: ", 42 | "deleteFailed": "Failed to delete the file. Please check the ComfyUI server.", 43 | "updated": "Updated", 44 | "updatedFailed": "Failed to update. Please check the ComfyUI server.", 45 | "Invalid filename": "Invalid filename" 46 | } 47 | }, 48 | "filesList": { 49 | "searchInput": { 50 | "placeholder": "Filter by filename" 51 | }, 52 | "Added to Saves": "Added to Saves", 53 | "Failed to add to Saves": "Failed to add to Saves. Please check the ComfyUI server.", 54 | "You want to delete this file?": "You want to delete this file?", 55 | "Deleted the file": "Deleted the file ", 56 | "Failed to delete the file": "Failed to delete the file. Please check the ComfyUI server." 57 | }, 58 | "downloadHistory": { 59 | "Auto Refresh": "Auto Refresh" 60 | }, 61 | "sourcesTab": { 62 | "You want to delete this source": "You want to delete this source? ", 63 | "Deleted this source": "Deleted this source.", 64 | "Failed to delete this source": "Failed to delete this source.", 65 | "Add new source": "Add new source", 66 | "Subscribe": "Subscribe", 67 | "Subscribing...": "Subscribing..." 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /web/build/_app/immutable/chunks/singletons.Bc_UULDX.js: -------------------------------------------------------------------------------- 1 | import{w as u}from"./index.BlOrB6Vx.js";const b=globalThis.__sveltekit_sim1g4?.base??"",v=globalThis.__sveltekit_sim1g4?.assets??b,k="1714203421307",R="sveltekit:snapshot",T="sveltekit:scroll",w="sveltekit:states",I="sveltekit:pageurl",S="sveltekit:history",y="sveltekit:navigation",f={tap:1,hover:2,viewport:3,eager:4,off:-1,false:-1},_=location.origin;function N(e){if(e instanceof URL)return e;let t=document.baseURI;if(!t){const n=document.getElementsByTagName("base");t=n.length?n[0].href:document.URL}return new URL(e,t)}function U(){return{x:pageXOffset,y:pageYOffset}}function c(e,t){return e.getAttribute(`data-sveltekit-${t}`)}const d={...f,"":f.hover};function g(e){let t=e.assignedSlot??e.parentNode;return t?.nodeType===11&&(t=t.host),t}function L(e,t){for(;e&&e!==t;){if(e.nodeName.toUpperCase()==="A"&&e.hasAttribute("href"))return e;e=g(e)}}function O(e,t){let n;try{n=new URL(e instanceof SVGAElement?e.href.baseVal:e.href,document.baseURI)}catch{}const a=e instanceof SVGAElement?e.target.baseVal:e.target,r=!n||!!a||E(n,t)||(e.getAttribute("rel")||"").split(/\s+/).includes("external"),l=n?.origin===_&&e.hasAttribute("download");return{url:n,external:r,target:a,download:l}}function Y(e){let t=null,n=null,a=null,r=null,l=null,o=null,s=e;for(;s&&s!==document.documentElement;)a===null&&(a=c(s,"preload-code")),r===null&&(r=c(s,"preload-data")),t===null&&(t=c(s,"keepfocus")),n===null&&(n=c(s,"noscroll")),l===null&&(l=c(s,"reload")),o===null&&(o=c(s,"replacestate")),s=g(s);function i(h){switch(h){case"":case"true":return!0;case"off":case"false":return!1;default:return}}return{preload_code:d[a??"off"],preload_data:d[r??"off"],keepfocus:i(t),noscroll:i(n),reload:i(l),replace_state:i(o)}}function p(e){const t=u(e);let n=!0;function a(){n=!0,t.update(o=>o)}function r(o){n=!1,t.set(o)}function l(o){let s;return t.subscribe(i=>{(s===void 0||n&&i!==s)&&o(s=i)})}return{notify:a,set:r,subscribe:l}}function m(){const{set:e,subscribe:t}=u(!1);let n;async function a(){clearTimeout(n);try{const r=await fetch(`${v}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!r.ok)return!1;const o=(await r.json()).version!==k;return o&&(e(!0),clearTimeout(n)),o}catch{return!1}}return{subscribe:t,check:a}}function E(e,t){return e.origin!==_||!e.pathname.startsWith(t)}function x(e){e.client}const P={url:p({}),page:p({}),navigating:u(null),updated:m()};export{S as H,y as N,I as P,T as S,w as a,R as b,Y as c,P as d,b as e,L as f,O as g,f as h,E as i,x as j,_ as o,N as r,U as s}; 2 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # ComfyUI 浏览器 2 | 3 | 查看和管理 ComfyUI 的所有输出文件,并且添加收藏方便随时调用。 4 | 5 | 远程同步工作流到你的 Git 仓库,方便团队共享和版本管理。 6 | 7 | 还能订阅社区开放的工作流仓库,方便抄作业! 8 | 9 | https://github.com/talesofai/comfyui-browser/assets/828837/803ce57a-1cf2-4e1c-be17-0efab401ef54 10 | 11 | 也可以参考B站的中文解说: 12 | https://www.bilibili.com/video/BV1qc411m7Gp/ 13 | 14 | ## 功能 15 | 16 | - 浏览和管理你的 ComfyUI 输出文件 17 | - 添加工作流到收藏夹,方便管理和调用 18 | - 可以通过 Git 来远程同步收藏夹 19 | - 订阅工作流仓库,方便抄作业 20 | - 通过关键词搜索工作流 21 | 22 | 23 | ## 预览 24 | 25 | ![b359de5f6556649512e7ed8f812ba67d444be9914173e2467018450ce1a3ce1d](https://github.com/talesofai/comfyui-browser/assets/828837/4b0b0f4c-28a8-49ef-98c2-d293df5b7747) 26 | ![c91157bf819ef5b9a129976d9e45588106dd6c7ea39ecb0a22519acd72afc7ce](https://github.com/talesofai/comfyui-browser/assets/828837/ee3df970-017c-4825-ab5d-9465cdb77ed6) 27 | ![53053f43847da9597efebab207140eed703b8c7bbe8eb1e63ce5630b5d8c9a3f](https://github.com/talesofai/comfyui-browser/assets/828837/4acb522a-f21c-47ad-9a23-56b08c6e73a5) 28 | ![c7b93b2ec0891eb7cac1385505e855fb28934ec958f7b21cac53c9bf18e6136c](https://github.com/talesofai/comfyui-browser/assets/828837/ef0d5cd2-9238-4e80-9f65-0f7db05ffbf3) 29 | 30 | ## 安装方式 31 | 32 | ### ComfyUI Manager 33 | 安装[ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager), 在 Install Custom Node 中搜索 `comfyui-browser` 来安装。 34 | 35 | ### 手动 36 | 37 | 下载这个仓库的代码放到 `ComfyUI/custom_nodes` 目录下,并重启 ComfyUI。 38 | 39 | ```bash 40 | cd custom_nodes && git clone https://github.com/tzwm/comfyui-browser.git 41 | ``` 42 | 43 | ## 开发 44 | 45 | - 前置需求 46 | - 安装[Node](https://nodejs.org/en/download/current) 47 | 48 | - 使用的框架 49 | 50 | - 前端: [Svelte](https://kit.svelte.dev/) 51 | - 后端: [aiohttp](https://docs.aiohttp.org/)(和 ComfyUI 一样) 52 | 53 | - 目录介绍 54 | 55 | ``` 56 | ├── __init__.py (后端服务) 57 | ├── web (ComfyUI 加载的前端路径) 58 | ├── build (Svelte 的生成文件) 59 | └── index.js (和 ComfyUI 交互的前端代码) 60 | ├── svelte (前端主体部分) 61 | ``` 62 | 63 | - 开发和调试 64 | 65 | - 复制或者链接 `comfyui-browser` 到 `ComfyUI/custom_nodes/` 66 | - 启动服务端: `cd ComfyUI && python main.py --enable-cors-header` 67 | - 启动前端: `cd ComfyUI/custom_nodes/comfyui-browser/svelte && npm i && npm run dev` 68 | - 调试地址 `http://localhost:5173/?comfyUrl=http://localhost:8188` 69 | - `localhost:8188` 是 ComfyUI server 地址 70 | - `localhost:5173` 是 Vite dev server 71 | 72 | - 备注 73 | 74 | - 请尽量在 Windows 上测试, 因为我只有 Linux 和 macOS 75 | - 在 ComfyUI 中可以按 'B' 键来打开/关闭 Browser 76 | 77 | 78 | 79 | ## 更新记录 80 | 81 | 详见:[ChangeLog](CHANGELOG.md) 82 | 83 | ## 感谢 84 | 85 | - [ComfyUI](https://github.com/comfyanonymous/ComfyUI) 86 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from aiohttp import web 3 | 4 | import server 5 | 6 | from .utils import collections_path, browser_path, sources_path, download_logs_path, outputs_path 7 | from .routes import sources, collections, config, files, downloads, xyz_plot as xyz_plot_routes 8 | from .nodes import select_inputs, load_image_by_url, xyz_plot, dify_text_generator, upload_to_remote 9 | 10 | 11 | browser_app = web.Application() 12 | browser_app.add_routes([ 13 | web.get("/files", files.api_get_files), 14 | web.delete("/files", files.api_delete_file), 15 | web.put("/files", files.api_update_file), 16 | web.get("/files/view", files.api_view_file), 17 | 18 | web.post("/collections", collections.api_add_to_collections), 19 | web.post("/collections/workflows", collections.api_create_new_workflow), 20 | web.post("/collections/sync", collections.api_sync_my_collections), 21 | 22 | web.get("/sources", sources.api_get_sources), 23 | web.post("/sources", sources.api_create_source), 24 | web.delete("/sources/{name}", sources.api_delete_source), 25 | web.post("/sources/sync/{name}", sources.api_sync_source), 26 | web.get("/sources/all", sources.api_get_all_sources), 27 | 28 | web.get("/config", config.api_get_browser_config), 29 | web.put("/config", config.api_update_browser_config), 30 | 31 | web.post("/downloads", downloads.api_create_new_download), 32 | web.get("/downloads", downloads.api_list_downloads), 33 | web.get("/downloads/{uuid}", downloads.api_show_download), 34 | 35 | web.put("/xyz_plot/score", xyz_plot_routes.api_update_score), 36 | web.get("/xyz_plot/statistic", xyz_plot_routes.api_get_score_statistic), 37 | 38 | web.static("/web", os.path.join(browser_path, 'web/build')), 39 | 40 | web.static("/s/outputs", outputs_path()), 41 | web.static("/s/collections", collections_path()), 42 | web.static("/s/sources", sources_path()), 43 | ]) 44 | server.PromptServer.instance.app.add_subapp("/browser/", browser_app) 45 | 46 | WEB_DIRECTORY = "web" 47 | NODE_CLASS_MAPPINGS = { 48 | "LoadImageByUrl //Browser": load_image_by_url.LoadImageByUrl, 49 | "SelectInputs //Browser": select_inputs.SelectInputs, 50 | "XyzPlot //Browser": xyz_plot.XyzPlot, 51 | "DifyTextGenerator //Browser": dify_text_generator.DifyTextGenerator, 52 | "UploadToRemote //Browser": upload_to_remote.UploadToRemote, 53 | } 54 | NODE_DISPLAY_NAME_MAPPINGS = { 55 | "LoadImageByUrl //Browser": "Load Image By URL", 56 | "SelectInputs //Browser": "Select Node Inputs", 57 | "XyzPlot //Browser": "XYZ Plot", 58 | "DifyTextGenerator //Browser": "Dify Text Generator", 59 | "UploadToRemote //Browser": "Upload To Remote", 60 | } 61 | -------------------------------------------------------------------------------- /svelte/src/routes/NewDownload.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 |
50 |
51 |
52 | * Download URL 53 |
54 | 60 |
61 | 62 |
63 |
64 | Filename(leave this blank if you want to auto detect this) 65 |
66 | 71 |
72 | 73 | 74 |
75 |
76 | * Select the modal type 77 |
78 | 86 |
87 | 88 |
89 | 93 |
94 | 95 | 101 |
102 | 103 | 104 | -------------------------------------------------------------------------------- /svelte/src/routes/DownloadHistory.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 |
50 | 56 | 57 |
58 | {$t('downloadHistory.Auto Refresh')} 59 | 60 |
61 |
62 |
    63 | {#each downloads as dl} 64 |
  • 65 |

    {dl.filename ? dl.filename : dl.uuid}

    66 |

    67 | Created at: {dl.formattedCreatedAt} 68 | Updated at: {dl.formattedUpdatedAt} 69 |

    70 |

    URL: {dl.download_url}

    71 |

    Save in: {dl.save_in}

    72 | {#if dl.total_size !== 0} 73 |
    74 | {dl.formattedDownloadedSize}/{dl.formattedTotalSize} 75 | {dl.percentStr} 76 | 80 | 81 |
    82 | {:else} 83 |

    Result: {dl.result}

    84 | {/if} 85 |
  • 86 | {/each} 87 |
88 | -------------------------------------------------------------------------------- /svelte/src/lib/multi-dim-table/layout.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 26 |
27 | 28 | 93 | 94 |
95 |
96 | 98 | {#if sidebarItems} 99 | 108 | {/if} 109 |
110 |
111 | -------------------------------------------------------------------------------- /svelte/src/routes/xyz_plot/+page.svelte: -------------------------------------------------------------------------------- 1 | 78 | 79 | {#if typeof data.path === 'string' && data.path.length > 0} 80 | {#if loading} 81 |
82 | 83 |
84 | {:else if !payload} 85 | null 86 | {:else} 87 | { 92 | open(payload.workflow.url); 93 | }, 94 | }, 95 | ]} 96 | > 97 |
98 | XYZ Plot 99 |
`${d.axis}: ${d.key} - ${d.type}`) 103 | .join('\n')} 104 | > 105 | 106 |
107 |
108 |
109 | 114 |
115 |
116 | {/if} 117 | {:else} 118 | Invalid Path 119 | {/if} 120 | -------------------------------------------------------------------------------- /data/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "sources": [ 3 | { 4 | "title": "ComfyUI_examples", 5 | "author": "comfyanonymous", 6 | "url": "https://github.com/comfyanonymous/ComfyUI_examples", 7 | "type": "git", 8 | "description": "Examples of ComfyUI workflows" 9 | }, 10 | { 11 | "title": "ComfyUI-Workflows-ZHO", 12 | "author": "ZHO-ZHO-ZHO", 13 | "url": "https://github.com/ZHO-ZHO-ZHO/ComfyUI-Workflows-ZHO", 14 | "type": "git", 15 | "description": "我的 ComfyUI 工作流合集 | My ComfyUI workflows collection" 16 | }, 17 | { 18 | "title": "comfyui-workflow", 19 | "author": "hylarucoder", 20 | "url": "https://github.com/hylarucoder/comfyui-workflow", 21 | "type": "git", 22 | "description": "Some workflows that I have authored myself as well as some workflows that I have modified from online sources." 23 | }, 24 | { 25 | "title": "sharing-is-caring", 26 | "author": "fictions-ai", 27 | "url": "https://github.com/fictions-ai/sharing-is-caring", 28 | "type": "git", 29 | "description": "This repository is maintained by the fictions.ai team. We believe in the power of collaboration and the magic that happens when we share knowledge. Here, we'll be sharing our workflow, useful scripts, and tools related to A.I. generation." 30 | }, 31 | { 32 | "title": "Consume-ComfyUI-Workflows", 33 | "author": "C0nsumption", 34 | "url": "https://github.com/C0nsumption/Consume-ComfyUI-Workflows", 35 | "type": "git", 36 | "description": "A collection of some of my basic ComfyUI workflows. These are meant to act as building block to construct larger workflows of your own." 37 | }, 38 | { 39 | "title": "ChaosFlow", 40 | "author": "BoosterCore", 41 | "url": "https://github.com/BoosterCore/ChaosFlow", 42 | "type": "git", 43 | "description": "ChaosFlow is a code repository used to showcase some of my own ComfyUI workflow creations. The purpose is to share my exploration and practice with ComfyUI, and at the same time, I hope to receive feedback and suggestions from interested friends. ChaosFlow welcomes visual design enthusiasts of any level to browse and comment. You can also message me on GitHub, and I am very happy to communicate and learn with you." 44 | }, 45 | { 46 | "title": "awesome-comfyui-workflow", 47 | "author": "shadowcz007", 48 | "url": "https://github.com/shadowcz007/awesome-comfyui-workflow", 49 | "type": "git", 50 | "description": "Some workflows includes CUTE YOU, Real-time design, Extended image, AI Dialogue Game..." 51 | }, 52 | { 53 | "title": "IF-Animation-Workflows", 54 | "author": "if-ai", 55 | "url": "https://github.com/if-ai/IF-Animation-Workflows", 56 | "type": "git", 57 | "description": "This are a series of ComfyUI workflows that work together to create and repurpose animation" 58 | }, 59 | { 60 | "title": "wyrde-comfyui-workflows", 61 | "author": "wyrde", 62 | "url": "https://github.com/wyrde/wyrde-comfyui-workflows", 63 | "type": "git", 64 | "description": "some wyrde workflows for comfyUI" 65 | }, 66 | { 67 | "title": "Templates-ComfyUI-", 68 | "author": "atlasunified", 69 | "url": "https://github.com/atlasunified/Templates-ComfyUI-", 70 | "type": "git", 71 | "description": "Templates to view the variety of a prompt based on the samplers available in ComfyUI. Variety of sizes and singlular seed and random seed templates." 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /routes/collections.py: -------------------------------------------------------------------------------- 1 | from os import path, makedirs 2 | from aiohttp import web 3 | import shutil 4 | import time 5 | 6 | from ..utils import collections_path, get_parent_path, add_uuid_to_filename, \ 7 | config_path, get_config, git_init, run_cmd, git_remote_name 8 | 9 | 10 | # filename, folder_path, folder_type = 'outputs' | 'sources' 11 | async def api_add_to_collections(request): 12 | json_data = await request.json() 13 | filename = json_data.get('filename') 14 | if not filename: 15 | return web.Response(status=404) 16 | 17 | folder_path = json_data.get('folder_path', '') 18 | 19 | folder_type = json_data.get("folder_type", "outputs") 20 | parent_path = get_parent_path(folder_type) 21 | 22 | makedirs(collections_path(), exist_ok=True) 23 | 24 | source_file_path = path.join(parent_path, folder_path, filename) 25 | if not path.exists(source_file_path): 26 | return web.Response(status=404) 27 | 28 | new_filepath = path.join( 29 | collections_path(), 30 | add_uuid_to_filename(filename) 31 | ) 32 | 33 | if path.isdir(source_file_path): 34 | shutil.copytree(source_file_path, new_filepath) 35 | else: 36 | shutil.copy(source_file_path, new_filepath) 37 | 38 | return web.Response(status=201) 39 | 40 | # filename, content 41 | async def api_create_new_workflow(request): 42 | json_data = await request.json() 43 | filename = json_data.get('filename') 44 | content = json_data.get('content') 45 | 46 | if not (filename and content): 47 | return web.Response(status=404) 48 | 49 | new_filepath = path.join( 50 | collections_path(), 51 | add_uuid_to_filename(filename) 52 | ) 53 | with open(new_filepath, 'w', encoding='utf-8') as f: 54 | f.write(content) 55 | 56 | return web.Response(status=201) 57 | 58 | async def api_sync_my_collections(_): 59 | if not path.exists(config_path): 60 | return web.Response(status=404) 61 | 62 | config = get_config() 63 | git_repo = config.get('git_repo') 64 | if not git_repo: 65 | return web.Response(status=404) 66 | 67 | git_init() 68 | 69 | cmd = 'git status -s' 70 | ret = run_cmd(cmd, collections_path()) 71 | if len(ret.stdout) > 0: 72 | cmd = f'git add . && git commit -m "sync by comfyui-browser at {int(time.time())}"' 73 | ret = run_cmd(cmd, collections_path()) 74 | if not ret.returncode == 0: 75 | return web.json_response( 76 | { 'message': "\n".join([ret.stdout, ret.stderr]) }, 77 | status=500, 78 | ) 79 | 80 | cmd = f'git fetch {git_remote_name} -v' 81 | ret = run_cmd(cmd, collections_path()) 82 | if not ret.returncode == 0: 83 | return web.json_response( 84 | { 'message': "\n".join([ret.stdout, ret.stderr]) }, 85 | status=500, 86 | ) 87 | 88 | cmd = 'git branch --show-current' 89 | ret = run_cmd(cmd, collections_path()) 90 | branch = ret.stdout.replace('\n', '') 91 | 92 | cmd = f'git merge {git_remote_name}/{branch}' 93 | ret = run_cmd(cmd, collections_path(), log_code=False) 94 | 95 | cmd = f'git push {git_remote_name} {branch}' 96 | ret = run_cmd(cmd, collections_path()) 97 | if not ret.returncode == 0: 98 | return web.json_response( 99 | { 'message': "\n".join([ret.stdout, ret.stderr]) }, 100 | status=500, 101 | ) 102 | 103 | return web.Response(status=200) 104 | -------------------------------------------------------------------------------- /svelte/src/lib/multi-dim-table/table-cell-img.svelte: -------------------------------------------------------------------------------- 1 | 96 | 97 |
98 | 99 | {value.uuid} 105 | 106 | {#if _mode === TableMode.Score} 107 | 119 | {/if} 120 |
121 | -------------------------------------------------------------------------------- /routes/sources.py: -------------------------------------------------------------------------------- 1 | from os import path, scandir, makedirs 2 | import json 3 | from aiohttp import web, ClientSession, ClientTimeout 4 | import re 5 | import shutil 6 | import os, stat, errno 7 | 8 | from ..utils import sources_path, run_cmd, browser_path, git_remote_name 9 | 10 | 11 | def handle_remove_readonly(func, path, exc): 12 | excvalue = exc[1] 13 | if excvalue.errno == errno.EACCES: 14 | os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 15 | func(path) 16 | else: 17 | raise 18 | 19 | async def api_get_sources(_): 20 | if not path.exists(sources_path()): 21 | return web.json_response({ 22 | 'sources': [] 23 | }) 24 | 25 | sources = [] 26 | source_list = scandir(sources_path()) 27 | source_list = sorted(source_list, key=lambda f: (-f.stat().st_ctime)) 28 | for item in source_list: 29 | if not path.exists(item.path): 30 | continue 31 | 32 | if item.is_file(): 33 | continue 34 | 35 | cmd = f'git remote get-url {git_remote_name}' 36 | ret = run_cmd(cmd, path.join(item.path), log_cmd=False, log_code=False, log_message=False) 37 | if not (ret.returncode == 0 and len(ret.stdout) > 0): 38 | continue 39 | 40 | url = ret.stdout.split('\n')[0] 41 | name = path.basename(item.path) 42 | created_at = item.stat().st_ctime 43 | sources.append({ 44 | "name": name, 45 | "created_at": created_at, 46 | "url": url 47 | }) 48 | 49 | return web.json_response({ 50 | 'sources': sources 51 | }) 52 | 53 | # repo_url 54 | async def api_create_source(request): 55 | json_data = await request.json() 56 | repo_url = json_data['repo_url'] 57 | 58 | if not repo_url: 59 | return web.Response(status=400) 60 | 61 | makedirs(sources_path(), exist_ok=True) 62 | 63 | pattern = r'[\:\/]([a-zA-Z0-9-_]+)\/([a-zA-Z0-9-_]+)(\.git)?' 64 | ret = re.search(pattern, repo_url) 65 | author = ret.group(1) 66 | name = ret.group(2) 67 | if not (author and name): 68 | return web.Response(status=400, text='wrong url') 69 | 70 | cmd = f'git clone --depth 1 {repo_url} {author}-{name}' 71 | ret = run_cmd(cmd, sources_path()) 72 | if ret.returncode != 0: 73 | return web.Response(status=400, text=ret.stdout + ret.stderr) 74 | 75 | return web.Response(status=201) 76 | 77 | 78 | # name 79 | async def api_delete_source(request): 80 | name = request.match_info.get('name', None) 81 | 82 | if not name: 83 | return web.Response(status=401) 84 | 85 | target_path = path.join(sources_path(), name) 86 | if not path.exists(target_path): 87 | return web.Response(status=404) 88 | 89 | shutil.rmtree(target_path, onerror=handle_remove_readonly) 90 | return web.Response(status=200) 91 | 92 | # name 93 | async def api_sync_source(request): 94 | name = request.match_info.get('name', None) 95 | 96 | if not name: 97 | return web.Response(status=401) 98 | if not path.exists(path.join(sources_path(), name)): 99 | return web.Response(status=404) 100 | 101 | cmd = f'git pull' 102 | ret = run_cmd(cmd, path.join(sources_path(), name)) 103 | 104 | if ret.returncode == 0: 105 | return web.Response(status=200) 106 | else: 107 | return web.Response(status=400, text=ret.stdout + ret.stderr) 108 | 109 | async def api_get_all_sources(_): 110 | source_url = 'https://github.com/talesofai/comfyui-browser/raw/main/data/sources.json' 111 | file_path = path.join(browser_path, 'data/sources.json') 112 | timeout = ClientTimeout(connect=2, total=4) 113 | 114 | sources = { 115 | "sources": [] 116 | } 117 | try: 118 | async with ClientSession(timeout=timeout) as session: 119 | async with session.get(source_url) as resp: 120 | if resp.ok: 121 | ret = await resp.text() 122 | sources = json.loads(ret) 123 | except: 124 | with open(file_path, 'r', encoding="utf-8") as f: 125 | sources = json.load(f) 126 | 127 | return web.json_response(sources) 128 | -------------------------------------------------------------------------------- /routes/files.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | import json 3 | from os import path 4 | import os 5 | import shutil 6 | 7 | from ..utils import get_target_folder_files, get_parent_path, get_info_filename, \ 8 | image_extensions, video_extensions 9 | 10 | # folder_path, folder_type 11 | async def api_get_files(request): 12 | folder_path = request.query.get('folder_path', '') 13 | folder_type = request.query.get('folder_type', 'outputs') 14 | files = get_target_folder_files(folder_path, folder_type=folder_type) 15 | 16 | if files == None: 17 | return web.Response(status=404) 18 | 19 | return web.json_response({ 20 | 'files': files 21 | }) 22 | 23 | 24 | # filename, folder_path, folder_type 25 | async def api_delete_file(request): 26 | json_data = await request.json() 27 | filename = json_data['filename'] 28 | folder_path = json_data.get('folder_path', '') 29 | folder_type = json_data.get('folder_type', 'outputs') 30 | 31 | parent_path = get_parent_path(folder_type) 32 | target_path = path.join(parent_path, folder_path, filename) 33 | if not path.exists(target_path): 34 | return web.json_response(status=404) 35 | 36 | if path.isdir(target_path): 37 | shutil.rmtree(target_path) 38 | else: 39 | os.remove(target_path) 40 | info_file_path = get_info_filename(target_path) 41 | if path.exists(info_file_path): 42 | os.remove(info_file_path) 43 | 44 | return web.Response(status=201) 45 | 46 | 47 | # filename, folder_path, folder_type, new_data: {} 48 | async def api_update_file(request): 49 | json_data = await request.json() 50 | filename = json_data['filename'] 51 | folder_path = json_data.get('folder_path', '') 52 | folder_type = json_data.get('folder_type', 'outputs') 53 | parent_path = get_parent_path(folder_type) 54 | 55 | new_data = json_data.get('new_data', None) 56 | if not new_data: 57 | return web.Response(status=400) 58 | 59 | new_filename = new_data['filename'] 60 | notes = new_data['notes'] 61 | 62 | old_file_path = path.join(parent_path, folder_path, filename) 63 | new_file_path = path.join(parent_path, folder_path, new_filename) 64 | 65 | if not path.exists(old_file_path): 66 | return web.Response(status=404) 67 | 68 | if new_filename and filename != new_filename: 69 | shutil.move( 70 | old_file_path, 71 | new_file_path 72 | ) 73 | old_info_file_path = get_info_filename(old_file_path) 74 | if path.exists(old_info_file_path): 75 | new_info_file_path = get_info_filename(new_file_path) 76 | shutil.move( 77 | old_info_file_path, 78 | new_info_file_path 79 | ) 80 | 81 | if notes: 82 | extra = { 83 | "notes": notes 84 | } 85 | info_file_path = get_info_filename(new_file_path) 86 | with open(info_file_path, "w", encoding="utf-8") as outfile: 87 | json.dump(extra, outfile) 88 | 89 | return web.Response(status=201) 90 | 91 | # filename, folder_path, folder_type 92 | async def api_view_file(request): 93 | folder_type = request.query.get("folder_type", "outputs") 94 | folder_path = request.query.get("folder_path", "") 95 | filename = request.query.get("filename", None) 96 | if not filename: 97 | return web.Response(status=404) 98 | 99 | parent_path = get_parent_path(folder_type) 100 | file_path = path.join(parent_path, folder_path, filename) 101 | 102 | if not path.exists(file_path): 103 | return web.Response(status=404) 104 | 105 | with open(file_path, 'rb') as f: 106 | media_file = f.read() 107 | 108 | content_type = 'application/json' 109 | file_extension = path.splitext(filename)[1].lower() 110 | if file_extension in image_extensions: 111 | content_type = f'image/{file_extension[1:]}' 112 | if file_extension in video_extensions: 113 | content_type = f'video/{file_extension[1:]}' 114 | 115 | return web.Response( 116 | body=media_file, 117 | content_type=content_type, 118 | headers={"Content-Disposition": f"filename=\"{filename}\""} 119 | ) 120 | -------------------------------------------------------------------------------- /nodes/upload_to_remote.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import asyncio 3 | import json 4 | from PIL import Image 5 | from PIL.PngImagePlugin import PngInfo 6 | import numpy as np 7 | import io 8 | import base64 9 | from ..utils import http_client, log 10 | 11 | class UploadToRemote: 12 | CATEGORY = "Browser" 13 | 14 | RETURN_TYPES = () 15 | RETURN_NAMES = () 16 | 17 | FUNCTION = "run" 18 | 19 | OUTPUT_NODE = True 20 | 21 | 22 | @classmethod 23 | def INPUT_TYPES(s): 24 | return { 25 | "required": { 26 | "remote_url": ["STRING", {}], 27 | "extension": (['jpeg', 'webp', 'png', 'jpg', 'gif'], ), 28 | "quality": ("INT", {"default": 85, "min": 1, "max": 100, "step": 1}), 29 | "embed_workflow": (["false", "true"],), 30 | }, 31 | "optional": { 32 | "images": ["IMAGE", {}], 33 | "extra": ["STRING", {"forceInput": True}], 34 | "track_id": ["STRING", {"placeholder": "Optional. Post it as the track_id field."}], 35 | }, 36 | "hidden": { 37 | "unique_id": "UNIQUE_ID", 38 | "prompt": "PROMPT", 39 | } 40 | } 41 | 42 | 43 | def run(self, remote_url, extension='jpeg', quality=85, images=[], extra='', embed_workflow='false', track_id=None, unique_id=None, prompt=None): 44 | def process_images(images, extension='jpeg', quality=85, embed_workflow='false', prompt=None): 45 | results = list() 46 | for image in images: 47 | i = 255. * image.cpu().numpy() 48 | img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) 49 | 50 | # Delegate metadata/pnginfo 51 | if extension == 'webp': 52 | img_exif = img.getexif() 53 | if embed_workflow == 'true' and prompt is not None: 54 | img_exif[0x010f] = "Prompt:" + json.dumps(prompt) 55 | exif_data = img_exif.tobytes() 56 | else: 57 | metadata = PngInfo() 58 | if embed_workflow == 'true' and prompt is not None: 59 | metadata.add_text("prompt", json.dumps(prompt)) 60 | exif_data = metadata 61 | 62 | output = io.BytesIO() 63 | if extension in ["jpg", "jpeg"]: 64 | img.convert('RGB').save(output, quality=quality, optimize=True, format="JPEG") 65 | elif extension == 'webp': 66 | img.convert('RGB').save(output, quality=quality, exif=exif_data, format="WEBP") 67 | else: 68 | img.save(output, pnginfo=exif_data, optimize=True, format="PNG") 69 | image_bytes = output.getvalue() 70 | image_base64 = base64.b64encode(image_bytes) 71 | image_base64_str = image_base64.decode('utf-8') 72 | results.append(image_base64_str) 73 | 74 | return results 75 | 76 | async def callback(images, extra, remote_url, extension='jpeg', quality=85, embed_workflow='false', track_id=None, unique_id=None, prompt=None): 77 | data = { 78 | "images": process_images(images, extension, quality, embed_workflow, prompt), 79 | "extra": extra, 80 | } 81 | if track_id: 82 | data['track_id'] = track_id 83 | if unique_id: 84 | data['unique_id'] = unique_id 85 | 86 | headers = { 87 | "Content-Type": "application/json", 88 | } 89 | data = json.dumps(data).encode('utf-8') 90 | log(f"uploading {track_id} to {remote_url}") 91 | res = http_client().post(remote_url, data=data, headers=headers) 92 | log(f"uploaded {track_id}: {res.status_code} {res.text}") 93 | # TODO: check the response 94 | 95 | 96 | threading.Thread( 97 | target=asyncio.run, 98 | args=(callback(images, extra, remote_url, extension, quality, embed_workflow, track_id, unique_id, prompt),), 99 | ).start() 100 | 101 | return () 102 | -------------------------------------------------------------------------------- /routes/xyz_plot.py: -------------------------------------------------------------------------------- 1 | import os 2 | from aiohttp import web 3 | import datetime 4 | import csv 5 | import json 6 | 7 | import folder_paths 8 | 9 | LOG_FILE_NAME = 'score_log.csv' 10 | 11 | async def api_update_score(request): 12 | body = await request.json() 13 | user_uuid = body.get("user", "anonymous") 14 | source = body.get("source") 15 | score = body.get("score") 16 | 17 | source_arr = source.split(":") 18 | if len(source_arr) != 5: 19 | return web.Response(status=404) 20 | 21 | log_path = os.path.join( 22 | folder_paths.get_output_directory(), 23 | source_arr[0], 24 | LOG_FILE_NAME, 25 | ) 26 | 27 | dtstr = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S%Z") 28 | 29 | # created_at, user_uuid, score, ix, iy, iz, index 30 | new_line = ",".join([dtstr, user_uuid, str(score), source_arr[1], source_arr[2], source_arr[3], source_arr[4]]) + '\n' 31 | if not os.path.exists(log_path): 32 | with open(log_path, "w", encoding="utf-8") as f: 33 | f.write(new_line) 34 | else: 35 | lines = [] 36 | with open(log_path, "r", encoding="utf-8") as f: 37 | lines = f.readlines() 38 | pattern = ",".join([source_arr[1], source_arr[2], source_arr[3], source_arr[4]]) 39 | lines = [line for line in lines if pattern not in line] 40 | lines.append(new_line) 41 | with open(log_path, "w", encoding="utf-8") as f: 42 | f.writelines(lines) 43 | 44 | return web.Response(status=200) 45 | 46 | async def api_get_score_statistic(request): 47 | target_path = request.query.get('path') 48 | if not target_path: 49 | return web.Response(status=404) 50 | 51 | #/browser/s/outputs/xyz_plot_4/result.json 52 | if len(target_path.split('/')) < 5: 53 | return web.Response(status=400) 54 | 55 | target_path = target_path.split('/')[4] 56 | log_path = os.path.join( 57 | folder_paths.get_output_directory(), 58 | target_path, 59 | LOG_FILE_NAME, 60 | ) 61 | if not os.path.exists(log_path): 62 | return web.Response(status=404) 63 | 64 | data = [] 65 | with open(log_path, 'r', encoding='utf-8') as f: 66 | reader = csv.reader(f) 67 | for row in reader: 68 | data.append(row) 69 | 70 | statistic = {} 71 | for row in data: 72 | if not row[3] in statistic: 73 | statistic[row[3]] = { 'total': 0 } 74 | if not row[4] in statistic[row[3]]: 75 | statistic[row[3]][row[4]] = { 'total': 0 } 76 | if not row[5] in statistic[row[3]][row[4]]: 77 | statistic[row[3]][row[4]][row[5]] = 0 78 | 79 | statistic[row[3]][row[4]][row[5]] += int(row[2]) 80 | statistic[row[3]][row[4]]['total'] += int(row[2]) 81 | statistic[row[3]]['total'] += int(row[2]) 82 | 83 | result_path = os.path.join( 84 | folder_paths.get_output_directory(), 85 | target_path, 86 | 'result.json', 87 | ) 88 | xyz_result = [] 89 | with open(result_path, 'r', encoding='utf-8') as f: 90 | xyz_result = json.load(f)['result'] 91 | 92 | stat_ret = [] 93 | for ix, _ in enumerate(xyz_result): 94 | vy = xyz_result[ix]['children'] 95 | rx = { 96 | 'type': 'axis', 97 | 'score': statistic.get(str(ix), {}).get('total', 0), 98 | 'children': [], 99 | } 100 | for iy, _ in enumerate(vy): 101 | vz = vy[iy]['children'] 102 | ry = { 103 | 'type': 'axis', 104 | 'score': statistic.get(str(ix), {}).get(str(iy), {}).get('total', 0), 105 | 'children': [], 106 | } 107 | if vz[0]['type'] == 'img': 108 | ry['children'].append({ 109 | 'type': 'axis', 110 | 'score': statistic.get(str(ix), {}).get(str(iy), {}).get('-1', 0), 111 | }) 112 | else: 113 | for iz, _ in enumerate(vz): 114 | ry['children'].append({ 115 | 'type': 'axis', 116 | 'score': statistic.get(str(ix), {}).get(str(iy), {}).get(str(iz), 0), 117 | }) 118 | rx['children'].append(ry) 119 | stat_ret.append(rx) 120 | 121 | 122 | return web.json_response({ 123 | 'result': stat_ret, 124 | }) 125 | -------------------------------------------------------------------------------- /svelte/src/routes/utils.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import type Toast from './Toast.svelte'; 3 | 4 | export type FOLDER_TYPES = 'outputs' | 'collections' | 'sources'; 5 | 6 | export const IMAGE_EXTS = ['png', 'webp', 'jpeg', 'jpg', 'gif']; 7 | export const VIDEO_EXTS = ['mp4', 'webm', 'mov', 'avi', 'mkv']; 8 | export const JSON_EXTS = ['json']; 9 | export const WHITE_EXTS = ['html', 'image', 'video', 'json', 'dir']; 10 | 11 | const localStorageKey = 'comfyui-browser'; 12 | 13 | function getFileUrl(comfyUrl: string, folderType: string, file: any) { 14 | if (file.folder_path) { 15 | return `${comfyUrl}/browser/s/${folderType}/${file.folder_path}/${file.name}`; 16 | } else { 17 | return `${comfyUrl}/browser/s/${folderType}/${file.name}`; 18 | } 19 | } 20 | 21 | function findFile(filename: string, exts: Array, files: Array) { 22 | let fn: any = filename.split('.'); 23 | fn.pop(); 24 | fn = fn.join('.'); 25 | return files.find((f: any) => { 26 | const fa = f.name.split('.'); 27 | const extname = fa.pop().toLowerCase(); 28 | const name = fa.join('.'); 29 | 30 | return name === fn && exts.includes(extname); 31 | }); 32 | } 33 | 34 | function processFile( 35 | file: any, 36 | folderType: FOLDER_TYPES, 37 | comfyUrl: string, 38 | files: Array, 39 | ) { 40 | const extname = file.name.split('.').pop().toLowerCase(); 41 | if (WHITE_EXTS.includes(extname)) { 42 | file['fileType'] = extname; 43 | if (extname === 'json') { 44 | if (findFile(file.name, IMAGE_EXTS.concat(VIDEO_EXTS), files)) { 45 | return; 46 | } 47 | } 48 | } 49 | if (IMAGE_EXTS.includes(extname)) { 50 | file['fileType'] = 'image'; 51 | } 52 | if (VIDEO_EXTS.includes(extname)) { 53 | file['fileType'] = 'video'; 54 | } 55 | if (! file['fileType']) { 56 | return; 57 | } 58 | 59 | file['url'] = getFileUrl(comfyUrl, folderType, file); 60 | if (['image', 'video'].includes(file['fileType'])) { 61 | file['previewUrl'] = getFileUrl(comfyUrl, folderType, file); 62 | 63 | let jsonFile = findFile(file.name, JSON_EXTS, files); 64 | if (jsonFile) { 65 | file['url'] = getFileUrl(comfyUrl, folderType, jsonFile); 66 | } 67 | } 68 | 69 | const d = dayjs.unix(file.created_at); 70 | file['formattedDatetime'] = d.format('YYYY-MM-DD HH:mm:ss'); 71 | file['formattedSize'] = formatFileSize(file['bytes']); 72 | 73 | return file; 74 | } 75 | 76 | function processDir(dir: any) { 77 | dir['fileType'] = 'dir'; 78 | 79 | const newFolderPath = dir.folder_path ? `${dir.folder_path}/${dir.name}` : dir.name; 80 | dir['path'] = newFolderPath; 81 | 82 | const d = dayjs.unix(dir.created_at); 83 | dir['formattedDatetime'] = d.format('YYYY-MM-DD HH-mm-ss'); 84 | 85 | dir['formattedSize'] = '0 KB'; 86 | return dir; 87 | } 88 | 89 | export async function fetchFiles( 90 | folderType: FOLDER_TYPES, 91 | comfyUrl: string, 92 | folderPath?: string, 93 | ) { 94 | let url = comfyUrl + '/browser/files?folder_type=' + folderType; 95 | if (folderPath) { 96 | url = url + `&folder_path=${folderPath}&`; 97 | } 98 | 99 | const res = await fetch(url); 100 | const ret = await res.json(); 101 | 102 | let files = ret.files; 103 | let newFiles: Array = []; 104 | files.forEach((f: any) => { 105 | let newFile; 106 | if (f.type === 'dir') { 107 | newFile = processDir(f); 108 | } else { 109 | newFile = processFile(f, folderType, comfyUrl, files); 110 | } 111 | 112 | if (newFile) { 113 | newFiles.push(newFile); 114 | } 115 | }); 116 | 117 | return newFiles; 118 | } 119 | 120 | export function onScroll(showCursor: number, filesLen: number) { 121 | if (showCursor >= filesLen) { 122 | return showCursor; 123 | } 124 | 125 | const documentHeight = document.documentElement.scrollHeight; 126 | const scrollPosition = window.innerHeight + window.scrollY; 127 | if (scrollPosition >= documentHeight) { 128 | return showCursor + 10; 129 | } 130 | 131 | return showCursor; 132 | } 133 | 134 | export async function onLoadWorkflow(file: any, comfyApp: any, toast: Toast) { 135 | const res = await fetch(file.url); 136 | const blob = await res.blob(); 137 | const fileObj = new File([blob], file.name, { 138 | type: res.headers.get('Content-Type') || '', 139 | }); 140 | const f = comfyApp.loadGraphData.bind(comfyApp); 141 | comfyApp.loadGraphData = async function(graphData: any) { 142 | const modal = window.top?.document.getElementById('comfy-browser-dialog'); 143 | if (modal) { 144 | modal.style.display = 'none'; 145 | } 146 | await f(graphData); 147 | } 148 | await comfyApp.handleFile(fileObj); 149 | 150 | toast.show(false, 'Loaded', 'No workflow found here'); 151 | } 152 | 153 | 154 | export function getLocalConfig() { 155 | let localConfigStr = localStorage.getItem(localStorageKey); 156 | let localConfig: any = {}; 157 | 158 | if (localConfigStr) { 159 | localConfig = JSON.parse(localConfigStr); 160 | } 161 | 162 | return localConfig; 163 | } 164 | 165 | export function setLocalConfig(key: string, value: any) { 166 | let localConfig = getLocalConfig(); 167 | localConfig[key] = value; 168 | localStorage.setItem(localStorageKey, JSON.stringify(localConfig)); 169 | } 170 | 171 | export function formatFileSize(size: number) { 172 | if (size / 1024 / 1024 > 1) { 173 | return (size / 1024 / 1024).toFixed(2) + ' MB'; 174 | } else { 175 | return Math.round(size / 1024) + ' KB'; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /web/build/_app/immutable/chunks/index.m7aY60kV.js: -------------------------------------------------------------------------------- 1 | import{r as h,n as p,m as v,p as E,k as N,q as C,j as w,v as j,w as B,x as b,y as I,z as q,A as H}from"./scheduler.DD_VFgMU.js";let $=!1;function L(){$=!0}function P(){$=!1}function T(e,t,n,i){for(;e>1);n(s)<=i?e=s+1:t=s}return e}function z(e){if(e.hydrate_init)return;e.hydrate_init=!0;let t=e.childNodes;if(e.nodeName==="HEAD"){const r=[];for(let l=0;l0&&t[n[s]].claim_order<=l?s+1:T(1,s,_=>t[n[_]].claim_order,l))-1;i[r]=n[o]+1;const f=o+1;n[f]=r,s=Math.max(f,s)}const c=[],a=[];let u=t.length-1;for(let r=n[s]+1;r!=0;r=i[r-1]){for(c.push(t[r-1]);u>=r;u--)a.push(t[u]);u--}for(;u>=0;u--)a.push(t[u]);c.reverse(),a.sort((r,l)=>r.claim_order-l.claim_order);for(let r=0,l=0;r=c[l].claim_order;)l++;const o=le.removeEventListener(t,n,i)}function ne(e,t,n){n==null?e.removeAttribute(t):e.getAttribute(t)!==n&&e.setAttribute(t,n)}function ie(e){return e.dataset.svelteH}function re(e){return e===""?null:+e}function G(e){return Array.from(e.childNodes)}function R(e){e.claim_info===void 0&&(e.claim_info={last_index:0,total_claimed:0})}function S(e,t,n,i,s=!1){R(e);const c=(()=>{for(let a=e.claim_info.last_index;a=0;a--){const u=e[a];if(t(u)){const r=n(u);return r===void 0?e.splice(a,1):e[a]=r,s?r===void 0&&e.claim_info.last_index--:e.claim_info.last_index=a,u}}return i()})();return c.claim_order=e.claim_info.total_claimed,e.claim_info.total_claimed+=1,c}function A(e,t,n,i){return S(e,s=>s.nodeName===t,s=>{const c=[];for(let a=0;as.removeAttribute(a))},()=>i(t))}function se(e,t,n){return A(e,t,n,O)}function le(e,t,n){return A(e,t,n,F)}function U(e,t){return S(e,n=>n.nodeType===3,n=>{const i=""+t;if(n.data.startsWith(i)){if(n.data.length!==i.length)return n.splitText(i.length)}else n.data=i},()=>y(t),!0)}function ae(e){return U(e," ")}function ue(e,t){t=""+t,e.data!==t&&(e.data=t)}function ce(e,t){e.value=t??""}function fe(e,t,n,i){n==null?e.style.removeProperty(t):e.style.setProperty(t,n,i?"important":"")}function oe(e,t,n){for(let i=0;i{m.delete(e),i&&(n&&e.d(1),i())}),e.o(t)}else i&&i()}function pe(e,t,n){const i=e.$$.props[t];i!==void 0&&(e.$$.bound[i]=n,n(e.$$.ctx[i]))}function ye(e){e&&e.c()}function xe(e,t){e&&e.l(t)}function W(e,t,n){const{fragment:i,after_update:s}=e.$$;i&&i.m(t,n),w(()=>{const c=e.$$.on_mount.map(I).filter(N);e.$$.on_destroy?e.$$.on_destroy.push(...c):h(c),e.$$.on_mount=[]}),s.forEach(w)}function J(e,t){const n=e.$$;n.fragment!==null&&(j(n.after_update),h(n.on_destroy),n.fragment&&n.fragment.d(t),n.on_destroy=n.fragment=null,n.ctx=[])}function K(e,t){e.$$.dirty[0]===-1&&(q.push(e),H(),e.$$.dirty.fill(0)),e.$$.dirty[t/31|0]|=1<{const g=x.length?x[0]:_;return l.ctx&&s(l.ctx[f],l.ctx[f]=g)&&(!l.skip_bound&&l.bound[f]&&l.bound[f](g),o&&K(e,f)),_}):[],l.update(),o=!0,h(l.before_update),l.fragment=i?i(l.ctx):!1,t.target){if(t.hydrate){L();const f=G(t.target);l.fragment&&l.fragment.l(f),f.forEach(M)}else l.fragment&&l.fragment.c();t.intro&&V(e.$$.fragment),W(e,t.target,t.anchor),P(),E()}b(r)}class ve{$$=void 0;$$set=void 0;$destroy(){J(this,1),this.$destroy=p}$on(t,n){if(!N(n))return p;const i=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return i.push(n),()=>{const s=i.indexOf(n);s!==-1&&i.splice(s,1)}}$set(t){this.$$set&&!C(t)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}}const Q="4";typeof window<"u"&&(window.__svelte||(window.__svelte={v:new Set})).v.add(Q);export{ie as A,te as B,Z as C,ce as D,pe as E,oe as F,de as G,re as H,ve as S,Y as a,he as b,ae as c,V as d,ee as e,M as f,O as g,se as h,ge as i,G as j,ne as k,fe as l,y as m,U as n,ue as o,me as p,_e as q,ye as r,k as s,$e as t,xe as u,W as v,J as w,D as x,F as y,le as z}; 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI Browser 2 | 3 | [中文说明](README_CN.md) 4 | 5 | This is an image/video/workflow browser and manager for ComfyUI. 6 | You can sync your workflows to a remote Git repository and use them everywhere. 7 | 8 | Welcome to submit your workflow source by submitting [an issue](https://github.com/talesofai/comfyui-browser/issues/new?assignees=tzwm&labels=workflow-repo&projects=&template=new-workflow-repository.md&title=New+workflow+repo%3A). Let's build the workflows together. 9 | 10 | https://github.com/talesofai/comfyui-browser/assets/828837/803ce57a-1cf2-4e1c-be17-0efab401ef54 11 | 12 | ## Features 13 | 14 | - Browse and manage your images/videos/workflows in the output folder. 15 | - Add your workflows to the 'Saves' so that you can switch and manage them more easily. 16 | - Sync your 'Saves' anywhere by Git. 17 | - Subscribe workflow sources by Git and load them more easily. 18 | - Search your workflow by keywords. 19 | - Some useful custom nodes like xyz_plot, inputs_select. 20 | 21 | ## Custom Nodes 22 | 23 | #### Select Inputs 24 | 25 | - Select any inputs of the current graph. 26 | 27 | image 28 | 29 | 30 | #### XYZ Plot 31 | 32 | - Simple XYZ Plot by selecting inputs and filling in the values. 33 | 34 | image 35 | 36 | image 37 | 38 | 39 | ## Preview 40 | 41 | 42 | #### Outputs 43 | Outputs 44 | 45 | #### Saves 46 | Saves 47 | 48 | #### Sources 49 | Sources 50 | Recommended Sources 51 | 52 | #### Models 53 | Models 54 | 55 | #### Side Bar View 56 | SideBar 57 | 58 | ## Installation 59 | 60 | ### ComfyUI Manager 61 | 62 | Install [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager), search `comfyui-browser` in Install Custom Node and install it. 63 | 64 | #### Configuring 65 | 66 | In your `comfyui-browser` directory, you can add a `config.json` to override 67 | the directories that `comfyui-browser` uses. Ex: 68 | 69 | ``` json 70 | { 71 | "collections": "/var/lib/comfyui/comfyui-browser-collections", 72 | "download_logs": "/var/lib/comfyui/comfyui-browser-download-logs", 73 | "outputs": "/var/lib/comfyui/outputs", 74 | "sources": "/var/lib/comfyui/comfyui-browser-sources" 75 | } 76 | ``` 77 | 78 | The default configuration values are: 79 | 80 | ``` json 81 | { 82 | "collections": "[comfyui-browser]/collections", 83 | "download_logs": "[comfyui-browser]/download-logs", 84 | "outputs": "[comfyui]/outputs", 85 | "sources": "[comfyui-browser]/sources" 86 | } 87 | ``` 88 | 89 | Where `[comfyui-browser]` is the automatically determined path of your 90 | `comfyui-browser` installation, and `[comfyui]` is the automatically determined 91 | path of your `comfyui` server. Notably, the `outputs` directory defaults to the 92 | `--output-directory` argument to `comfyui` itself, or the default path that 93 | `comfyui` wishes to use for the `--output-directory` argument. 94 | 95 | ### Manually 96 | 97 | Clone this repo into the `custom_nodes` folder and restart the ComfyUI. 98 | 99 | ```bash 100 | cd custom_nodes && git clone https://github.com/tzwm/comfyui-browser.git 101 | ``` 102 | 103 | ## Notes 104 | 105 | - Your 'Saves' are stored in the `collections` configuration value. See Configuring for its default, and how to set the path 107 | to something different. 108 | - Press 'B' to toggle the Browser dialog in the ComfyUI. 109 | 110 | 111 | ## Development 112 | 113 | - Prerequisite 114 | - Install [Node](https://nodejs.org/en/download/current) 115 | 116 | 117 | - Framework 118 | 119 | - Frontend: [Svelte](https://kit.svelte.dev/) 120 | - Backend: [aiohttp](https://docs.aiohttp.org/)(the same as ComfyUI) 121 | 122 | - Project Structure 123 | 124 | ``` 125 | ├── __init__.py (Backend Server) 126 | ├── web (Frontend code loaded by ComfyUI) 127 | ├── build (Built in Svelte) 128 | └── index.js (Frontend that interact with ComfyUI) 129 | ├── svelte (Frontend in the Modal as a iframe, written in Svelte) 130 | ``` 131 | 132 | - Build and Run 133 | 134 | - Copy or link `comfyui-browser` to `ComfyUI/custom_nodes/` 135 | - Start backend by `cd ComfyUI && python main.py --enable-cors-header` 136 | - Start frontend by `cd ComfyUI/custom_nodes/comfyui-browser/svelte && npm i && npm run dev` 137 | - Open and debug by `http://localhost:5173/?comfyUrl=http://localhost:8188` 138 | - It will use `localhost:8188` as ComfyUI server 139 | - `localhost:5173` is a Vite dev server 140 | 141 | - Notes 142 | 143 | - Please try to test on Windows, because I only have Linux/macOS 144 | 145 | 146 | ## TODO 147 | 148 | - [x] Sync collections to remote git repository 149 | - [x] Add external git repository to local collections 150 | - [ ] Search workflow by node name and model name 151 | 152 | 153 | ## ChangeLog 154 | 155 | To see [ChangeLog](CHANGELOG.md). 156 | 157 | 158 | ## Credits 159 | 160 | - [ComfyUI](https://github.com/comfyanonymous/ComfyUI) 161 | -------------------------------------------------------------------------------- /routes/downloads.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | import os 3 | import shutil 4 | import time 5 | import asyncio 6 | import json 7 | import threading 8 | from aiohttp import web 9 | from tqdm import tqdm 10 | 11 | import folder_paths 12 | 13 | from ..utils import download_logs_path, log, http_client 14 | 15 | def parse_options_header(content_disposition): 16 | param, options = '', {} 17 | split_header = content_disposition.split(';') 18 | 19 | # Extract the first parameter 20 | if len(split_header) > 0: 21 | param = split_header[0].strip() 22 | 23 | # Extract the options 24 | for option in split_header[1:]: 25 | option_split = option.split('=') 26 | if len(option_split) == 2: 27 | key = option_split[0].strip() 28 | value = option_split[1].strip().strip('"') 29 | options[key] = value 30 | 31 | return param, options 32 | 33 | 34 | # credit: https://gist.github.com/phineas-pta/d73f9a035b05f8e923af8c01df057175 35 | async def download_by_requests(uuid:str, download_url:str, save_in:str, filename:str="", overwrite:bool=False, chunk_size:int=1): 36 | log_file_path = path.join(download_logs_path(), uuid + '.json') 37 | base_info = { 38 | 'uuid': uuid, 39 | 'download_url': download_url, 40 | 'save_in': save_in, 41 | 'filename': filename, 42 | 'overwrite': overwrite, 43 | 'method': 'requests', 44 | 'result': 'connecting', 45 | 'total_size': 0, 46 | 'downloaded_size': 0, 47 | 'created_at': int(time.time()), 48 | 'updated_at': int(time.time()), 49 | } 50 | with open(log_file_path, 'w', encoding='utf-8') as log_file: 51 | json.dump(base_info, log_file) 52 | 53 | HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"} 54 | 55 | with http_client().get(download_url, headers=HEADERS, stream=True) as resp: 56 | MISSING_FILENAME = f"unkwown_{uuid}" 57 | # get file name 58 | if filename == "": 59 | if content_disposition := resp.headers.get("Content-Disposition"): 60 | param, options = parse_options_header(content_disposition) 61 | if param == "attachment": 62 | filename = options.get("filename", MISSING_FILENAME) 63 | else: 64 | fileext = path.splitext(filename)[-1] 65 | if fileext != "": 66 | filename = path.basename(download_url) 67 | if filename == "": 68 | filename = MISSING_FILENAME 69 | 70 | base_info['filename'] = filename 71 | with open(log_file_path, 'w', encoding='utf-8') as log_file: 72 | json.dump(base_info, log_file) 73 | 74 | target_path = path.join(folder_paths.models_dir, save_in, filename) 75 | base_info['save_in'] = target_path 76 | if not overwrite and path.exists(target_path): 77 | result = f'Already exists: {target_path}' 78 | base_info['result'] = result 79 | with open(log_file_path, 'w', encoding='utf-8') as log_file: 80 | json.dump(base_info, log_file) 81 | log(result) 82 | return 83 | 84 | # download file 85 | tmp_target_path = target_path + '.downloading' 86 | TOTAL_SIZE = int(resp.headers.get("Content-Length", 0)) 87 | CHUNK_SIZE = chunk_size * 10**6 88 | base_info['total_size'] = TOTAL_SIZE 89 | base_info['result'] = resp.reason 90 | log('Download to ' + target_path) 91 | with ( 92 | open(tmp_target_path, mode="wb") as file, 93 | open(log_file_path, 'w', encoding='utf-8') as log_file, 94 | tqdm(total=TOTAL_SIZE, desc=f"download {filename}", unit="B", unit_scale=True) as bar, 95 | ): 96 | for data in resp.iter_content(chunk_size=CHUNK_SIZE): 97 | size = file.write(data) 98 | bar.update(size) 99 | base_info['downloaded_size'] += size 100 | base_info['updated_at'] = int(time.time()) 101 | log_file.seek(0) 102 | json.dump(base_info, log_file) 103 | log_file.write('\n' * 2) 104 | 105 | shutil.move(tmp_target_path, target_path) 106 | 107 | 108 | # download_url, filename, save_in, overwrite 109 | async def api_create_new_download(request): 110 | json_data = await request.json() 111 | download_url = json_data.get('download_url', None) 112 | save_in = json_data.get('save_in', None) 113 | filename = json_data.get('filename', '') 114 | overwrite = json_data.get('overwrite', False) 115 | 116 | if not (download_url and save_in): 117 | return web.Response(status=400) 118 | 119 | if '..' in save_in: 120 | return web.Response(status=400) 121 | 122 | threading.Thread( 123 | target=asyncio.run, 124 | args=(download_by_requests(str(int(time.time())), download_url, save_in, filename, overwrite),) 125 | ).start() 126 | 127 | return web.json_response(status=201) 128 | 129 | async def api_list_downloads(_): 130 | download_logs = [] 131 | folder_listing = os.scandir(download_logs_path()) 132 | folder_listing = sorted(folder_listing, key=lambda f: (f.is_file(), -f.stat().st_ctime)) 133 | for item in folder_listing: 134 | if not path.exists(item.path): 135 | continue 136 | 137 | name = path.basename(item.path) 138 | ext = path.splitext(name)[1].lower() 139 | if name == '' or name[0] == '.': 140 | continue 141 | if item.is_file() and not ext in ['.json']: 142 | continue 143 | 144 | info = {} 145 | with open(item.path, 'r') as f: 146 | try: 147 | info = json.load(f) 148 | except: 149 | pass 150 | 151 | if 'uuid' in info: 152 | download_logs.append(info) 153 | 154 | return web.json_response({ 155 | 'download_logs': download_logs, 156 | }) 157 | 158 | # uuid 159 | async def api_show_download(request): 160 | uuid = request.match_info.get('uuid', '') 161 | if uuid == '': 162 | return web.Response(status=400) 163 | 164 | target_path = path.join(download_logs_path(), uuid + '.json') 165 | if not path.exists(target_path): 166 | return web.Response(status=404) 167 | 168 | download_log = {} 169 | with open(target_path, 'r') as file: 170 | download_log = json.load(file) 171 | 172 | return web.json_response(download_log) 173 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import json 3 | from os import path, scandir, makedirs 4 | import subprocess 5 | import time 6 | from typing import TypedDict, List 7 | import requests 8 | from requests.adapters import HTTPAdapter, Retry 9 | 10 | import folder_paths 11 | from comfy.cli_args import args 12 | 13 | SERVER_BASE_URL = f'http://{args.listen}:{args.port}' 14 | # To support IPv6 15 | if ':' in args.listen: 16 | SERVER_BASE_URL = f'http://[{args.listen}]:{args.port}' 17 | 18 | browser_path = path.dirname(__file__) 19 | config_path = path.join(browser_path, 'config.json') 20 | 21 | image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'] 22 | video_extensions = ['.mp4', '.mov', '.avi', '.webm', '.mkv'] 23 | white_extensions = ['.json', '.html'] + image_extensions + video_extensions 24 | 25 | info_file_suffix = '.info' 26 | 27 | git_remote_name = 'origin' 28 | 29 | def http_client(): 30 | adapter = HTTPAdapter(max_retries=Retry(3, backoff_factor=0.1)) 31 | http = requests.session() 32 | http.mount('http://', adapter) 33 | http.mount('https://', adapter) 34 | 35 | return http 36 | 37 | 38 | @functools.cache 39 | def get_config(): 40 | return { 41 | "collections": path.join(browser_path, 'collections'), 42 | "download_logs": path.join(browser_path, 'download_logs'), 43 | "outputs": output_directory_from_comfyui(), 44 | "sources": path.join(browser_path, 'sources'), 45 | } | load_config() 46 | 47 | def load_config(): 48 | if not path.exists(config_path): 49 | return {} 50 | else: 51 | with open(config_path, 'r') as f: 52 | return json.load(f) 53 | 54 | @functools.cache 55 | def collections_path(): 56 | return get_config()['collections'] 57 | 58 | @functools.cache 59 | def download_logs_path(): 60 | return get_config()['download_logs'] 61 | 62 | @functools.cache 63 | def outputs_path(): 64 | return get_config()['outputs'] 65 | 66 | @functools.cache 67 | def sources_path(): 68 | return get_config()['sources'] 69 | 70 | 71 | class FileInfoDict(TypedDict): 72 | type: str 73 | name: str 74 | bytes: int 75 | created_at: float 76 | folder_path: str 77 | notes: str 78 | 79 | def log(message): 80 | print('[comfyui-browser] ' + message) 81 | 82 | def run_cmd(cmd, run_path, log_cmd=True, log_code=True, log_message=True): 83 | if log_cmd: 84 | log(f'running: {cmd}') 85 | 86 | ret = subprocess.run( 87 | f'cd {run_path} && {cmd}', 88 | shell=True, 89 | stdout=subprocess.PIPE, 90 | stderr=subprocess.PIPE, 91 | encoding="UTF-8" 92 | ) 93 | if log_code: 94 | if ret.returncode == 0: 95 | log('successed') 96 | else: 97 | log('failed') 98 | if log_message: 99 | if (len(ret.stdout) > 0 or len(ret.stderr) > 0): 100 | log(ret.stdout + ret.stderr) 101 | 102 | return ret 103 | 104 | # folder_type = 'outputs', 'collections', 'sources' 105 | def get_parent_path(folder_type: str): 106 | if folder_type == 'collections': 107 | return collections_path() 108 | if folder_type == 'sources': 109 | return sources_path() 110 | 111 | # outputs 112 | return outputs_path() 113 | 114 | # folder_type = 'outputs', 'collections', 'sources' 115 | def get_target_folder_files(folder_path: str, folder_type: str = 'outputs'): 116 | if '..' in folder_path: 117 | return None 118 | 119 | parent_path = get_parent_path(folder_type) 120 | files: List[FileInfoDict] = [] 121 | target_path = path.join(parent_path, folder_path) 122 | 123 | if not path.exists(target_path): 124 | return [] 125 | 126 | folder_listing = scandir(target_path) 127 | folder_listing = sorted(folder_listing, key=lambda f: (f.is_file(), -f.stat().st_ctime)) 128 | for item in folder_listing: 129 | if not path.exists(item.path): 130 | continue 131 | 132 | name = path.basename(item.path) 133 | ext = path.splitext(name)[1].lower() 134 | if name == '' or name[0] == '.': 135 | continue 136 | if item.is_file(): 137 | if not (ext in white_extensions): 138 | continue 139 | 140 | created_at = item.stat().st_ctime 141 | info_file_path = get_info_filename(item.path) 142 | info_data = {} 143 | if path.exists(info_file_path): 144 | with open(info_file_path, 'r') as f: 145 | info_data = json.load(f) 146 | if item.is_file(): 147 | bytes = item.stat().st_size 148 | files.append({ 149 | "type": "file", 150 | "name": name, 151 | "bytes": bytes, 152 | "created_at": created_at, 153 | "folder_path": folder_path, 154 | "notes": info_data.get("notes", "") 155 | }) 156 | elif item.is_dir(): 157 | files.append({ 158 | "type": "dir", 159 | "name": name, 160 | "bytes": 0, 161 | "created_at": created_at, 162 | "folder_path": folder_path, 163 | "notes": info_data.get("notes", "") 164 | }) 165 | 166 | return files 167 | 168 | def get_info_filename(filename): 169 | return path.splitext(filename)[0] + info_file_suffix 170 | 171 | def add_uuid_to_filename(filename): 172 | name, ext = path.splitext(filename) 173 | return f'{name}_{int(time.time())}{ext}' 174 | 175 | def output_directory_from_comfyui(): 176 | if args.output_directory: 177 | return path.abspath(args.output_directory) 178 | else: 179 | return folder_paths.get_output_directory() 180 | 181 | def git_init(): 182 | if not path.exists(path.join(collections_path(), '.git')): 183 | run_cmd('git init', collections_path()) 184 | 185 | ret = run_cmd('git config user.name', collections_path(), 186 | log_cmd=False, log_code=False, log_message=False) 187 | if len(ret.stdout) == 0: 188 | ret = run_cmd('whoami', collections_path(), 189 | log_cmd=False, log_code=False, log_message=False) 190 | username = ret.stdout.rstrip("\n") 191 | run_cmd(f'git config user.name "{username}"', collections_path()) 192 | 193 | ret = run_cmd('git config user.email', collections_path(), 194 | log_cmd=False, log_code=False, log_message=False) 195 | if len(ret.stdout) == 0: 196 | ret = run_cmd('hostname', collections_path(), 197 | log_cmd=False, log_code=False, log_message=False) 198 | hostname = ret.stdout.rstrip("\n") 199 | run_cmd(f'git config user.email "{hostname}"', collections_path()) 200 | 201 | for dir in [ 202 | collections_path(), 203 | sources_path(), 204 | download_logs_path(), 205 | outputs_path(), 206 | ]: 207 | makedirs(dir, exist_ok=True) 208 | -------------------------------------------------------------------------------- /web/build/_app/immutable/entry/app.D5ml1A7d.js: -------------------------------------------------------------------------------- 1 | const __vite__fileDeps=["../nodes/0.BLschTxR.js","../chunks/preload-helper.Dch09mLN.js","../chunks/runtime.CA0Siqq1.js","../chunks/index.BlOrB6Vx.js","../chunks/scheduler.DD_VFgMU.js","../chunks/_commonjsHelpers.Cpj98o6Y.js","../chunks/index.m7aY60kV.js","../assets/0.yjAznyz0.css","../nodes/1.E1uHrSfC.js","../chunks/singletons.Bc_UULDX.js","../nodes/2.-Y4AVwMq.js","../nodes/3.BEouhoXU.js","../chunks/each.-gASlQSi.js","../nodes/4.DteQa3LM.js"],__vite__mapDeps=i=>i.map(i=>__vite__fileDeps[i]); 2 | import{_ as P}from"../chunks/preload-helper.Dch09mLN.js";import{s as z,a as C,o as M,t as U,b as R}from"../chunks/scheduler.DD_VFgMU.js";import{S as B,i as F,s as G,e as u,c as H,a as w,t as m,b as N,d as p,f as d,g as J,h as K,j as Q,k as y,l as E,m as W,n as X,o as Y,p as D,q as g,r as b,u as V,v as k,w as v}from"../chunks/index.m7aY60kV.js";const le={};function Z(o){let e,n,r;var i=o[1][0];function c(t,f){return{props:{data:t[3],form:t[2]}}}return i&&(e=g(i,c(o)),o[15](e)),{c(){e&&b(e.$$.fragment),n=u()},l(t){e&&V(e.$$.fragment,t),n=u()},m(t,f){e&&k(e,t,f),w(t,n,f),r=!0},p(t,f){if(f&2&&i!==(i=t[1][0])){if(e){D();const s=e;m(s.$$.fragment,1,0,()=>{v(s,1)}),N()}i?(e=g(i,c(t)),t[15](e),b(e.$$.fragment),p(e.$$.fragment,1),k(e,n.parentNode,n)):e=null}else if(i){const s={};f&8&&(s.data=t[3]),f&4&&(s.form=t[2]),e.$set(s)}},i(t){r||(e&&p(e.$$.fragment,t),r=!0)},o(t){e&&m(e.$$.fragment,t),r=!1},d(t){t&&d(n),o[15](null),e&&v(e,t)}}}function x(o){let e,n,r;var i=o[1][0];function c(t,f){return{props:{data:t[3],$$slots:{default:[ie]},$$scope:{ctx:t}}}}return i&&(e=g(i,c(o)),o[14](e)),{c(){e&&b(e.$$.fragment),n=u()},l(t){e&&V(e.$$.fragment,t),n=u()},m(t,f){e&&k(e,t,f),w(t,n,f),r=!0},p(t,f){if(f&2&&i!==(i=t[1][0])){if(e){D();const s=e;m(s.$$.fragment,1,0,()=>{v(s,1)}),N()}i?(e=g(i,c(t)),t[14](e),b(e.$$.fragment),p(e.$$.fragment,1),k(e,n.parentNode,n)):e=null}else if(i){const s={};f&8&&(s.data=t[3]),f&65591&&(s.$$scope={dirty:f,ctx:t}),e.$set(s)}},i(t){r||(e&&p(e.$$.fragment,t),r=!0)},o(t){e&&m(e.$$.fragment,t),r=!1},d(t){t&&d(n),o[14](null),e&&v(e,t)}}}function ee(o){let e,n,r;var i=o[1][1];function c(t,f){return{props:{data:t[4],form:t[2]}}}return i&&(e=g(i,c(o)),o[13](e)),{c(){e&&b(e.$$.fragment),n=u()},l(t){e&&V(e.$$.fragment,t),n=u()},m(t,f){e&&k(e,t,f),w(t,n,f),r=!0},p(t,f){if(f&2&&i!==(i=t[1][1])){if(e){D();const s=e;m(s.$$.fragment,1,0,()=>{v(s,1)}),N()}i?(e=g(i,c(t)),t[13](e),b(e.$$.fragment),p(e.$$.fragment,1),k(e,n.parentNode,n)):e=null}else if(i){const s={};f&16&&(s.data=t[4]),f&4&&(s.form=t[2]),e.$set(s)}},i(t){r||(e&&p(e.$$.fragment,t),r=!0)},o(t){e&&m(e.$$.fragment,t),r=!1},d(t){t&&d(n),o[13](null),e&&v(e,t)}}}function te(o){let e,n,r;var i=o[1][1];function c(t,f){return{props:{data:t[4],$$slots:{default:[ne]},$$scope:{ctx:t}}}}return i&&(e=g(i,c(o)),o[12](e)),{c(){e&&b(e.$$.fragment),n=u()},l(t){e&&V(e.$$.fragment,t),n=u()},m(t,f){e&&k(e,t,f),w(t,n,f),r=!0},p(t,f){if(f&2&&i!==(i=t[1][1])){if(e){D();const s=e;m(s.$$.fragment,1,0,()=>{v(s,1)}),N()}i?(e=g(i,c(t)),t[12](e),b(e.$$.fragment),p(e.$$.fragment,1),k(e,n.parentNode,n)):e=null}else if(i){const s={};f&16&&(s.data=t[4]),f&65575&&(s.$$scope={dirty:f,ctx:t}),e.$set(s)}},i(t){r||(e&&p(e.$$.fragment,t),r=!0)},o(t){e&&m(e.$$.fragment,t),r=!1},d(t){t&&d(n),o[12](null),e&&v(e,t)}}}function ne(o){let e,n,r;var i=o[1][2];function c(t,f){return{props:{data:t[5],form:t[2]}}}return i&&(e=g(i,c(o)),o[11](e)),{c(){e&&b(e.$$.fragment),n=u()},l(t){e&&V(e.$$.fragment,t),n=u()},m(t,f){e&&k(e,t,f),w(t,n,f),r=!0},p(t,f){if(f&2&&i!==(i=t[1][2])){if(e){D();const s=e;m(s.$$.fragment,1,0,()=>{v(s,1)}),N()}i?(e=g(i,c(t)),t[11](e),b(e.$$.fragment),p(e.$$.fragment,1),k(e,n.parentNode,n)):e=null}else if(i){const s={};f&32&&(s.data=t[5]),f&4&&(s.form=t[2]),e.$set(s)}},i(t){r||(e&&p(e.$$.fragment,t),r=!0)},o(t){e&&m(e.$$.fragment,t),r=!1},d(t){t&&d(n),o[11](null),e&&v(e,t)}}}function ie(o){let e,n,r,i;const c=[te,ee],t=[];function f(s,_){return s[1][2]?0:1}return e=f(o),n=t[e]=c[e](o),{c(){n.c(),r=u()},l(s){n.l(s),r=u()},m(s,_){t[e].m(s,_),w(s,r,_),i=!0},p(s,_){let l=e;e=f(s),e===l?t[e].p(s,_):(D(),m(t[l],1,1,()=>{t[l]=null}),N(),n=t[e],n?n.p(s,_):(n=t[e]=c[e](s),n.c()),p(n,1),n.m(r.parentNode,r))},i(s){i||(p(n),i=!0)},o(s){m(n),i=!1},d(s){s&&d(r),t[e].d(s)}}}function L(o){let e,n=o[7]&&O(o);return{c(){e=J("div"),n&&n.c(),this.h()},l(r){e=K(r,"DIV",{id:!0,"aria-live":!0,"aria-atomic":!0,style:!0});var i=Q(e);n&&n.l(i),i.forEach(d),this.h()},h(){y(e,"id","svelte-announcer"),y(e,"aria-live","assertive"),y(e,"aria-atomic","true"),E(e,"position","absolute"),E(e,"left","0"),E(e,"top","0"),E(e,"clip","rect(0 0 0 0)"),E(e,"clip-path","inset(50%)"),E(e,"overflow","hidden"),E(e,"white-space","nowrap"),E(e,"width","1px"),E(e,"height","1px")},m(r,i){w(r,e,i),n&&n.m(e,null)},p(r,i){r[7]?n?n.p(r,i):(n=O(r),n.c(),n.m(e,null)):n&&(n.d(1),n=null)},d(r){r&&d(e),n&&n.d()}}}function O(o){let e;return{c(){e=W(o[8])},l(n){e=X(n,o[8])},m(n,r){w(n,e,r)},p(n,r){r&256&&Y(e,n[8])},d(n){n&&d(e)}}}function se(o){let e,n,r,i,c;const t=[x,Z],f=[];function s(l,h){return l[1][1]?0:1}e=s(o),n=f[e]=t[e](o);let _=o[6]&&L(o);return{c(){n.c(),r=G(),_&&_.c(),i=u()},l(l){n.l(l),r=H(l),_&&_.l(l),i=u()},m(l,h){f[e].m(l,h),w(l,r,h),_&&_.m(l,h),w(l,i,h),c=!0},p(l,[h]){let I=e;e=s(l),e===I?f[e].p(l,h):(D(),m(f[I],1,1,()=>{f[I]=null}),N(),n=f[e],n?n.p(l,h):(n=f[e]=t[e](l),n.c()),p(n,1),n.m(r.parentNode,r)),l[6]?_?_.p(l,h):(_=L(l),_.c(),_.m(i.parentNode,i)):_&&(_.d(1),_=null)},i(l){c||(p(n),c=!0)},o(l){m(n),c=!1},d(l){l&&(d(r),d(i)),f[e].d(l),_&&_.d(l)}}}function re(o,e,n){let{stores:r}=e,{page:i}=e,{constructors:c}=e,{components:t=[]}=e,{form:f}=e,{data_0:s=null}=e,{data_1:_=null}=e,{data_2:l=null}=e;C(r.page.notify);let h=!1,I=!1,A=null;M(()=>{const a=r.page.subscribe(()=>{h&&(n(7,I=!0),U().then(()=>{n(8,A=document.title||"untitled page")}))});return n(6,h=!0),a});function T(a){R[a?"unshift":"push"](()=>{t[2]=a,n(0,t)})}function $(a){R[a?"unshift":"push"](()=>{t[1]=a,n(0,t)})}function q(a){R[a?"unshift":"push"](()=>{t[1]=a,n(0,t)})}function S(a){R[a?"unshift":"push"](()=>{t[0]=a,n(0,t)})}function j(a){R[a?"unshift":"push"](()=>{t[0]=a,n(0,t)})}return o.$$set=a=>{"stores"in a&&n(9,r=a.stores),"page"in a&&n(10,i=a.page),"constructors"in a&&n(1,c=a.constructors),"components"in a&&n(0,t=a.components),"form"in a&&n(2,f=a.form),"data_0"in a&&n(3,s=a.data_0),"data_1"in a&&n(4,_=a.data_1),"data_2"in a&&n(5,l=a.data_2)},o.$$.update=()=>{o.$$.dirty&1536&&r.page.set(i)},[t,c,f,s,_,l,h,I,A,r,i,T,$,q,S,j]}class _e extends B{constructor(e){super(),F(this,e,re,se,z,{stores:9,page:10,constructors:1,components:0,form:2,data_0:3,data_1:4,data_2:5})}}const ce=[()=>P(()=>import("../nodes/0.BLschTxR.js"),__vite__mapDeps([0,1,2,3,4,5,6,7]),import.meta.url),()=>P(()=>import("../nodes/1.E1uHrSfC.js"),__vite__mapDeps([8,4,6,9,3]),import.meta.url),()=>P(()=>import("../nodes/2.-Y4AVwMq.js"),__vite__mapDeps([10,2,3,4,5,6]),import.meta.url),()=>P(()=>import("../nodes/3.BEouhoXU.js"),__vite__mapDeps([11,4,6,12,5,2,3]),import.meta.url),()=>P(()=>import("../nodes/4.DteQa3LM.js"),__vite__mapDeps([13,4,6,12,3,5]),import.meta.url)],ue=[],me={"/":[3],"/xyz_plot":[4,[2]]},pe={handleError:({error:o})=>{console.error(o)}};export{me as dictionary,pe as hooks,le as matchers,ce as nodes,_e as root,ue as server_loads}; 3 | -------------------------------------------------------------------------------- /svelte/src/routes/FilesList.svelte: -------------------------------------------------------------------------------- 1 | 120 | 121 | 138 | 139 |
140 | {#each files 141 | .filter((f) => searchRegex.test(f.name.toLowerCase())) 142 | .slice(0, showCursor) as file} 143 | {#if WHITE_EXTS.includes(file.fileType)} 144 |
145 |
146 | 147 |
148 | 149 |

150 | {file.name} 151 |

152 | 155 | 158 | 159 |
160 | {#if comfyApp && file.type != 'dir'} 161 | 166 | {/if} 167 | 172 | 181 |
182 |
183 | {/if} 184 | {/each} 185 |
186 | 187 |
188 | {#if files.length > showCursor} 189 | 190 | {:else} 191 |

No more content.

192 | {/if} 193 |
194 | 195 | {#if files.length === 0} 196 |
197 | 198 | {#if loaded} 199 | {$t('common.emptyList')} 200 | {:else} 201 | {$t('common.loading')} 202 | {/if} 203 | 204 |
205 | {/if} 206 | 207 | {#if scrollTop >= 300} 208 | 213 | {/if} 214 | -------------------------------------------------------------------------------- /svelte/src/routes/Navbar.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
30 | 110 | 111 | 130 | 138 | 145 |
146 | -------------------------------------------------------------------------------- /web/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |

No more content.

No data
24 | 25 | 49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /nodes/xyz_plot.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import time 3 | import json 4 | from PIL import Image 5 | import numpy as np 6 | import os 7 | import copy 8 | 9 | import folder_paths 10 | 11 | from ..utils import SERVER_BASE_URL, http_client 12 | 13 | class XyzPlot: 14 | CATEGORY = "Browser" 15 | 16 | RETURN_TYPES = () 17 | RETURN_NAMES = () 18 | 19 | FUNCTION = "run" 20 | 21 | OUTPUT_NODE = True 22 | 23 | # xyz_data = { 24 | # "source_unique_id": "", 25 | # "output_folder_name": "", 26 | # "x_index": 0, 27 | # "y_index": 0, 28 | # } 29 | @classmethod 30 | def INPUT_TYPES(s): 31 | return { 32 | "required": { 33 | "images": ["IMAGE", {}], 34 | "input_x": ["INPUT", {}], 35 | "input_y": ["INPUT", {}], 36 | "value_x": ["STRING", {"multiline": True, "placeholder": 'X values split by semicolon such as "1girl; 1boy"'}], 37 | "value_y": ["STRING", {"multiline": True, "placeholder": 'Y values split by semicolon such as "1girl; 1boy"'}], 38 | "value_z": ["STRING", {"multiline": True, "placeholder": 'Z values split by semicolon such as "1girl; 1boy"'}], 39 | "output_folder_name": ["STRING", {"default": "xyz_plot"}], 40 | }, 41 | "optional": { 42 | "input_z": ["INPUT", {}], 43 | }, 44 | "hidden": { 45 | "prompt": "PROMPT", 46 | "unique_id": "UNIQUE_ID", 47 | "extra_pnginfo": "EXTRA_PNGINFO", 48 | # "xyz_data": "XYZ", 49 | }, 50 | } 51 | 52 | @classmethod 53 | def IS_CHANGED(s, *args): 54 | return True 55 | 56 | def __init__(self) -> None: 57 | self.output_folder_name = os.path.join(folder_paths.get_output_directory(), "xyz_plot") 58 | self.x_index = 0 59 | self.y_index = 0 60 | 61 | @staticmethod 62 | def get_filename(ix, iy, iz, i): 63 | if (iz >= 0): 64 | return f"x{ix}_y{iy}_z{iz}_{i}.jpeg" 65 | else: 66 | return f"x{ix}_y{iy}_{i}.jpeg" 67 | 68 | @staticmethod 69 | def get_preview_url(folder_name, filename): 70 | return f"/browser/files/view?folder_type=outputs&filename={filename}&folder_path={folder_name}" 71 | 72 | def save_images(self, images): 73 | os.makedirs(self.output_folder_name, exist_ok=True) 74 | 75 | for index, image in enumerate(images): 76 | i = 255. * image.cpu().numpy() 77 | img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) 78 | img = img.convert('RGB') 79 | filename = self.get_filename(self.x_index, self.y_index, self.z_index, index) 80 | target_path = os.path.join(self.output_folder_name, filename) 81 | img.save(target_path, 'JPEG', quality=90) 82 | 83 | def run(self, images, input_x, input_y, value_x, value_y, output_folder_name, prompt, unique_id, input_z=None, value_z="", extra_pnginfo=None): 84 | self.output_folder_name = os.path.join( 85 | folder_paths.get_output_directory(), 86 | output_folder_name, 87 | ) 88 | 89 | if 'xyz_data' in prompt[unique_id]['inputs']: 90 | self.x_index = prompt[unique_id]['inputs']['xyz_data']['x_index'] 91 | self.y_index = prompt[unique_id]['inputs']['xyz_data']['y_index'] 92 | self.z_index = prompt[unique_id]['inputs']['xyz_data']['z_index'] 93 | self.save_images(images) 94 | return () 95 | 96 | if os.path.exists(self.output_folder_name): 97 | shutil.move(self.output_folder_name, self.output_folder_name + f'_old_{int(time.time())}') 98 | 99 | def filter_values(value): 100 | return list(filter(lambda x: x != '', value.split(";"))) 101 | 102 | def queue_new_prompt(prompt): 103 | data = json.dumps({ 104 | 'prompt': prompt 105 | }).encode('utf-8') 106 | 107 | # for some special network environments like AutoDL 108 | proxies = {"http": "", "https": ""} 109 | return http_client().post(SERVER_BASE_URL + '/prompt', data=data, proxies=proxies) 110 | 111 | 112 | batch_size = len(images) 113 | values_x = filter_values(value_x) 114 | values_y = filter_values(value_y) 115 | values_z = filter_values(value_z) 116 | new_prompt = copy.deepcopy(prompt) 117 | ret = [] 118 | for ix, vx in enumerate(values_x): 119 | vx = vx.strip() 120 | row = [] 121 | for iy, vy in enumerate(values_y): 122 | vy = vy.strip() 123 | new_prompt[input_x["node_id"]]["inputs"][input_x["widget_name"]] = vx 124 | new_prompt[input_y["node_id"]]["inputs"][input_y["widget_name"]] = vy 125 | 126 | new_prompt[unique_id]['inputs']['xyz_data'] = { 127 | "source_unique_id": unique_id, 128 | "output_folder_name": output_folder_name, 129 | "x_index": ix, 130 | "y_index": iy, 131 | "z_index": -1, 132 | } 133 | 134 | ceil = [] 135 | if (input_z and len(values_z) > 0): 136 | for iz, vz in enumerate(values_z): 137 | vz = vz.strip() 138 | new_prompt[input_z["node_id"]]["inputs"][input_z["widget_name"]] = vz 139 | new_prompt[unique_id]['inputs']['xyz_data']['z_index'] = iz 140 | queue_new_prompt(new_prompt) 141 | zCeil = [] 142 | for i in range(batch_size): 143 | filename = self.get_filename(ix, iy, iz, i) 144 | preview_url = self.get_preview_url(output_folder_name, filename) 145 | zCeil.append({ 146 | "uuid": ":".join([output_folder_name, str(ix), str(iy), str(iz), str(i)]), 147 | "type": "img", 148 | "src": preview_url, 149 | }) 150 | ceil.append({ 151 | "type": "axis", 152 | "value": vz, 153 | "children": zCeil, 154 | }) 155 | else: 156 | queue_new_prompt(new_prompt) 157 | for i in range(batch_size): 158 | filename = self.get_filename(ix, iy, -1, i) 159 | preview_url = self.get_preview_url(output_folder_name, filename) 160 | ceil.append({ 161 | "uuid": ":".join([output_folder_name, str(ix), str(iy), "-1", str(i)]), 162 | "type": "img", 163 | "src": preview_url, 164 | }) 165 | 166 | row.append({ 167 | "type": "axis", 168 | "value": vy, 169 | "children": ceil, 170 | }) 171 | 172 | ret.append({ 173 | "type": "axis", 174 | "value": vx, 175 | "children": row, 176 | }) 177 | 178 | # Check if the directory exists 179 | try: 180 | os.makedirs(self.output_folder_name, exist_ok=True) 181 | except Exception as e: 182 | raise Exception(f"Failed to create directory: {e}") 183 | 184 | browser_base_url = f"/browser/s/outputs/{output_folder_name}" 185 | retData = { 186 | "result": ret, 187 | } 188 | if extra_pnginfo: 189 | workflow_filename = "workflow.json" 190 | with open(f"{self.output_folder_name}/{workflow_filename}", "w", encoding="utf-8") as f: 191 | json.dump(extra_pnginfo['workflow'], f) 192 | retData["workflow"] = { 193 | "url": f'{browser_base_url}/{workflow_filename}', 194 | } 195 | 196 | axisConst = ["X", "Y", "Z"] 197 | retData["annotations"] = [] 198 | for index, ele in enumerate([input_x, input_y, input_z]): 199 | if not ele: 200 | continue 201 | retData["annotations"].append({ 202 | "axis": axisConst[index], 203 | "key": f"#{ele['node_id']} {ele['node_title']}", 204 | "type": ele['widget_name'], 205 | }) 206 | 207 | 208 | target_path = f"{self.output_folder_name}/result.json" 209 | with open(target_path, "w", encoding="utf-8") as f: 210 | json.dump(retData, f) 211 | 212 | return { 213 | "ui": { 214 | "result_path": [f"/browser/web/xyz_plot.html?path={browser_base_url}/result.json"], 215 | }, 216 | "result": (), 217 | } 218 | -------------------------------------------------------------------------------- /svelte/src/routes/CollectionsTab.svelte: -------------------------------------------------------------------------------- 1 | 207 | 208 |
209 | 214 | 223 | 228 | 229 | 230 | 236 | {#if configGitRepo != config?.git_repo} 237 | 240 | {/if} 241 | 244 |
245 | 246 | 263 | 264 |
    265 | {#each files 266 | .filter((f) => searchRegex.test(f.name.toLowerCase()) || searchRegex.test(f.notes.toLowerCase())) 267 | .slice(0, showCursor) as file} 268 |
  • 269 | 270 |
    271 | updateFilename(e, file)} 275 | value={file.name} 276 | /> 277 | 280 | 281 |
    282 | {#if comfyApp && file.type != 'dir'} 283 | 288 | {/if} 289 | 298 |
    299 |
    300 | 301 |
    302 |