Skip to content

Commit 16ec788

Browse files
committed
Add Poses tab; Update Readme.
1 parent 47e4346 commit 16ec788

File tree

5 files changed

+105
-20
lines changed

5 files changed

+105
-20
lines changed

README.md

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@
44

55
Inspired by the Extra Network tabs in Automatic1111's WebUI, Extra Network Browser is a stand-alone take on the concept with additional features.
66

7-
Easily choose a LoRA, HyperNetwork, Embedding, Checkpoint, or Style visually and copy the trigger, keywords, and suggested weight *to the clipboard* for easy pasting into the application of your choice.
7+
Easily choose a **LoRA, HyperNetwork, Embedding, Checkpoint, or Style** visually and copy the trigger, keywords, and suggested weight to the clipboard for easy pasting into the application of your choice.
88

9-
Advantages over the Extra Network Tabs:
9+
Features of Extra Network Browser:
1010

1111
* Great for UI's like ComfyUI when used with nodes like [Lora Tag Loader](https://github.com/badjeff/comfyui_lora_tag_loader/) or [ComfyUI Prompt Control](https://github.com/asagi4/comfyui-prompt-control).
12-
* Considerably faster, loads thousands of LoRA.
12+
* Considerably fast, loads thousands of LoRA easily.
1313
* A [Styles](#styles) tab that parses a styles CSV for thumbnail previews just like networks.
1414
* [Keywords](#keywords) in the filename inside brackets [ ]'s are copied along with the LoRA trigger.
1515
* [Weights](#weights) placed in braces { }'s *(eg {1.0} or {0.7-0.8})* in the filename are automatically set in the LoRA's trigger.
1616
* Some characters not compatible with filenames are automatically converted from placeholders, such as ©️ to : *(for [keywords with weights](#weights))*
1717
* Sort by Name, Date Modified, or try Random sort for inspiration.
1818
* Support for multiple images per LoRA/model/etc in a [modal gallery](#modal) (including filename [search](#search)). Hover over a card & click the folder icon.
1919
* Support for [displaying a companion .txt file](#modal) to store descriptions, notes, and prompts. Hover over a card & click the document icon.
20+
* [Poses](#poses) tab to visually display OpenPose collections and examples.
2021
* [Gallery](#modal) tab for arbitrary image folders, such as saved generation results.
2122

2223
<br />
@@ -53,7 +54,7 @@ cd ../app && npm install
5354

5455
Then add content:
5556

56-
- Populate the folders in `api/networks/` with your files: `lora`, `checkpoints`, `embeddings`, `hypernets`, `styles`, and `gallery`.
57+
- Populate the folders in `api/networks/` with your files: `lora`, `checkpoints`, `embeddings`, `hypernets`, `styles`, `poses`, and `gallery`.
5758
- Edit `api/networks/styles.csv` with *(only)* `name,prompt` on the first line, and your styles (following the format shown [below](#styles)) on the subsequent lines.
5859

5960
***OR***
@@ -228,17 +229,49 @@ In this example, your matching image files in `api/networks/styles` would be:
228229

229230
---
230231

232+
<a id="poses"></a>
233+
### Poses:
234+
235+
The Poses tab ("P") is designed to work with OpenPose images used with ControlNet. In this tab, you can manage examples of those poses for easy browing.
236+
237+
This tab works somewhat differently than the model tabs, in that it looks for images rather than model files.<br />
238+
I've tried to somewhat match the format of many zipped packs I've seen, and as such I recommend the following folder structure:
239+
240+
```
241+
/api/poses/<pose>/
242+
/api/poses/<posefolder>/pose1.png
243+
/api/poses/<posefolder>/pose2.png
244+
/api/poses/<posefolder>/OpenPose/pose1.png #optional
245+
/api/poses/<posefolder>/OpenPose/pose2.png #optional
246+
/api/poses/<posefolder>/Depth/pose1.png #optional
247+
/api/poses/<posefolder>/Canny/pose2.png #optional
248+
```
249+
250+
The structure here is:
251+
* A general name for the pose folder *(e.g. "Heart Hands" or "T Pose")*
252+
* Example images with the pose inside that folder
253+
* OpenPose mannequin files inside a subfolder called OpenPose
254+
* Optionally any other ControlNet files stored likewise *(e.g. Depth, Canny, or Lineart folders)*
255+
256+
For best compatibility this tab uses **.png** instead of **.jpeg**.
257+
*This can be changed in `api/index.js` by editing `const ext ... "png" : imgExt`, on line 260 to `jpeg`, etc.*
258+
259+
---
260+
231261
<a id="gallery"></a>
232262
### Gallery:
233263

234-
The Gallery tab *("G")* looks for *subfolders* inside the `api/networks/gallery/` folder and displays a card for each, using a .jpeg matching the folder name in the `/gallery`.
264+
The Gallery tab ("G") looks for *subfolders* inside the `api/networks/gallery/` folder and displays a card for each, using a .jpeg matching the folder name in the `/gallery`.
235265
Examples:
236266
```
237-
api/networks/gallery/saved-images/ # Gallery folder with images
238-
api/networks/gallery/saved-images.jpeg # Gallery folder thumbnail image
239-
240-
api/networks/gallery/testing-images/ # Gallery folder with images
241-
api/networks/gallery/testing-images.jpeg # Gallery folder thumbnail image
267+
api/networks/gallery/saved-images.jpeg # Gallery folder thumbnail image
268+
api/networks/gallery/saved-images/one.jpeg # Gallery folder with images
269+
api/networks/gallery/saved-images/two.jpeg # Gallery folder with images
270+
api/networks/gallery/saved-images/three.jpeg # Gallery folder with images
271+
272+
api/networks/gallery/testing-images.jpeg # Gallery folder thumbnail image
273+
api/networks/gallery/testing-images/cat.jpeg # Gallery folder with images
274+
api/networks/gallery/testing-images/dog.jpeg # Gallery folder with images
242275
```
243276

244277
<a id="search"></a>

api/index.js

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ app.use('/styles', express.static('networks/styles'))
1919
app.use('/checkpoints', express.static('networks/checkpoints'))
2020
app.use('/embeddings', express.static('networks/embeddings'))
2121
app.use('/hypernets', express.static('networks/hypernets'))
22+
app.use('/poses', express.static('networks/poses'))
2223
app.use('/gallery', express.static('networks/gallery'))
2324

2425
// Endpoint to return images
@@ -94,15 +95,58 @@ app.get('/images', (req, res) => {
9495
let filename = (file.name.indexOf("." + imgExt) > -1) ? file.name : file.name + "." + imgExt
9596
let path = file.path.replace(file.name, '').replace('networks/', '').toLowerCase()
9697

97-
// This return only for files.map
98+
// This return is only for files.map
9899
return {
99100
filename: filename,
100101
path: path,
101102
}
102103
})
103104
res.json(images)
104105

106+
} else if (searchType == "poses") {
107+
// Poses searches for a list of png one level deep.
108+
109+
let pattern = ''
110+
let extraPattern = ''
111+
let _onlyDirectories = false
112+
let ext = "png"
113+
114+
if (searchTerm) {
115+
if (searchTerm.split(">").length > 1) {
116+
pattern = searchTerm.split(">")[0] ? `*${searchTerm.split(">")[0]}*` : '*'
117+
extraPattern = '/' + (searchTerm.split(">")[1] ? `*${searchTerm.split(">")[1]}*` : '*')
118+
extraPattern += "." + ext
119+
} else {
120+
pattern = `*${searchTerm}*`
121+
extraPattern += "." + ext
122+
}
123+
} else {
124+
pattern = '*'
125+
extraPattern += "." + ext
126+
}
127+
128+
const files = fg.globSync([`networks/${searchType}/*/${pattern}${extraPattern}`], { onlyDirectories: _onlyDirectories, dot: false, caseSensitiveMatch: false, stats: true })
129+
130+
images = files.flatMap(file => {
131+
132+
let filename = (file.name.indexOf("." + ext) > -1) ? file.name : file.name + "." + ext
133+
let path = file.path.replace(file.name, '').replace('networks/', '').toLowerCase()
134+
// Set the containing directory name as the nameplate.
135+
let name = /poses\/(.+?)\//.exec(path)[1]
136+
// Skip files with ". (\d+)", e.g. "filename. (2).png" etc.
137+
if (filename.match(/\. \([0-9]+\)\./)) { return [] }
138+
139+
// This return is only for files.flatMap
140+
return {
141+
filename: filename,
142+
path: path,
143+
name: name
144+
}
145+
})
146+
res.json(images)
147+
105148
} else {
149+
// Everything else searches for model files
106150
let ext = null
107151
if (searchType == "lora" || searchType == "checkpoints") {
108152
ext = "safetensors"
@@ -173,7 +217,7 @@ app.get('/images', (req, res) => {
173217
prompt = noext
174218
}
175219

176-
// This return only for files.map
220+
// This return is only for files.map
177221
return {
178222
filename: filename,
179223
path: path,
@@ -200,6 +244,7 @@ app.get('/moreImages', (req, res) => {
200244
const search = req.query.search || res.status(400).send({
201245
message: 'Missing query.'
202246
});
247+
const type = req.query.type
203248

204249
// Chars like {} break the glob search if unescaped.
205250
const filteredSearch = search.replaceAll('$','\\$')
@@ -212,11 +257,14 @@ app.get('/moreImages', (req, res) => {
212257
.replaceAll('{','\\{')
213258
.replaceAll('}','\\}')
214259

215-
const noext = filteredSearch.substring(filteredSearch.lastIndexOf('/') + 1).replace('.' + imgExt, '')
260+
const ext = (type == "poses") ? "png" : imgExt
261+
const noext = filteredSearch.substring(filteredSearch.lastIndexOf('/') + 1).replace('.' + ext, '')
216262
const searchPath = filteredSearch.substring(0, filteredSearch.lastIndexOf('/'))
217263

218-
const query = `networks/${searchPath}/${noext}(.*|).${imgExt}`
219-
const queryFolder = `networks/${searchPath}/${noext}/*.${imgExt}`
264+
// (.*|) is NOT REGEX. It's ( literal dot, wildstar; OR empty ) e.g. ( example". (2)".jpeg OR example"".jpeg )
265+
// Not to be confused with regex ".*"
266+
const query = `networks/${searchPath}/${noext}(.*|).${ext}`
267+
const queryFolder = `networks/${searchPath}/${noext}/*.${ext}`
220268
const files = fg.globSync([query, queryFolder], { dot: false, caseSensitiveMatch: false, stats: true })
221269

222270
const images = files.map(file => {

api/networks/init.bat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ mkdir checkpoints
44
mkdir embeddings
55
mkdir hypernets
66
mkdir styles
7+
mkdir poses
78
mkdir gallery
89
echo n | copy /-y styles.csv.example styles.csv

app/src/GetImages.jsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export default function GetImages() {
9292
const image = images[moreIndex]
9393
const query = encodeURIComponent(image.path + image.filename)
9494

95-
axios.get(`http://localhost:3000/moreImages?search=${query}`)
95+
axios.get(`http://localhost:3000/moreImages?type=${type}&search=${query}`)
9696
.then(response => {
9797
setMoreLoading(false)
9898
setMoreError(false)
@@ -152,9 +152,11 @@ export default function GetImages() {
152152
setMoreDocument(null)
153153

154154
const image = images[index]
155-
const file = encodeURIComponent(image.filename.substring(0, image.filename.lastIndexOf('.')) + '.txt')
155+
const file = (type == "poses") ?
156+
'readme.txt'
157+
: encodeURIComponent(image.filename.substring(0, image.filename.lastIndexOf('.')) + '.txt')
156158

157-
axios.get(`http://localhost:3000/${image.path}${file}`)
159+
axios.get(`http://localhost:3000/${image.path.replaceAll('#','%23')}${file}`)
158160
.then(response => {
159161
setMoreLoading(false)
160162
setMoreError(false)
@@ -223,6 +225,7 @@ export default function GetImages() {
223225
<span title="Embedding" className={`typeOption ${type == "embeddings" ? "highlight" : ""}`} onClick={() => handleTypeChange("embeddings")}>E</span>
224226
<span title="HyperNetwork" className={`typeOption ${type == "hypernets" ? "highlight" : ""}`} onClick={() => handleTypeChange("hypernets")}>H</span>
225227
<span title="Checkpoint" className={`typeOption ${type == "checkpoints" ? "highlight" : ""}`} onClick={() => handleTypeChange("checkpoints")}>C</span>
228+
<span title="Poses" className={`typeOption ${type == "Poses" ? "highlight" : ""}`} onClick={() => handleTypeChange("poses")}>P</span>
226229
<span title="Gallery" className={`typeOption ${type == "gallery" ? "highlight" : ""}`} onClick={() => handleTypeChange("gallery")}>G</span>
227230
</div>
228231
<input id="imgSearch" icon='search' placeholder='Search...'
@@ -261,7 +264,7 @@ export default function GetImages() {
261264
{!moreLoading && !moreError && images[moreIndex] && moreImages[0] && !moreDocument && moreImages.map((image, index) => (
262265
<div key={index+1000000} className="imgCard" onClick={() => {navigator.clipboard.writeText(images[moreIndex].prompt)}}>
263266
<img width="224" height="336"
264-
src={"http://localhost:3000/" + image.path.replace('#','%23') + encodeURIComponent(image.filename)}
267+
src={"http://localhost:3000/" + image.path.replaceAll('#','%23') + encodeURIComponent(image.filename)}
265268
loading={index <= 60 ? "eager" : "lazy"}
266269
title={image.filename}
267270
/>

app/src/Image.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export default function Image(props) {
22
return (
33
<div className="imgCard" onClick={() => {navigator.clipboard.writeText(props.prompt)}}>
44
<img width="224" height="336"
5-
src={"http://localhost:3000/" + props.path.replace('#','%23') + encodeURIComponent(props.filename)}
5+
src={"http://localhost:3000/" + props.path.replaceAll('#','%23') + encodeURIComponent(props.filename)}
66
loading={props.index <= 60 ? "eager" : "lazy"}
77
title={props.path + props.filename}
88
/>

0 commit comments

Comments
 (0)