5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/pyPromptChecker.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/config.ini:
--------------------------------------------------------------------------------
1 | [Location]
2 | ModelList=
3 | Favourites=
4 |
5 | [Window]
6 | AlwaysStartWithDarkMode=False
7 | AlwaysOpenBy=tab
8 | AskWhenQuit=True
9 |
10 | [Pixmap]
11 | PixmapSize=350
12 | RegionalPrompterPixmapSize=500
13 | ThumbnailPixmapSize=150
14 | ListViewPixmapSize=200
15 |
16 | [Features]
17 | SubDirectoryDepth=0
18 | UsesNumberAsTabName=False
19 | OpenWithShortenedWindow=False
20 | ModelListSearchApplyLora=True
21 | ModelListSearchApplyTi=True
22 |
23 | JsonSingle=filename
24 | JsonMultiple=directory
25 | JsonSelected=selected
26 |
27 | MoveDelete=True
28 | UseCopyInsteadOfMove=True
29 | AskIfDelete=True
30 | AskIfClearTrashBin=True
31 |
32 | TabNavigation=True
33 | TabNavigationMinimumTabs=2
34 |
35 | ThumbnailTabBar=True
36 | ThumbnailTabBarVertical=True
37 | HideNormalTabBar=False
38 |
39 | TabSearch=True
40 | HideNotMatchedTabs=False
41 |
42 | [Tab]
43 | HiresExtras=True
44 | CFG=True
45 | LoraAddNet=True
46 | TiledDiffusion=True
47 | ControlNet=True
48 | RegionalPrompter=True
49 |
50 | [Ignore]
51 | IgnoreIfDataIsNotEmbedded=False
52 |
53 | [Debug]
54 | ErrorList=OnIfNeeded
55 | # ErrorList=AlwaysOff
56 | # ErrorList=AlwaysOn
57 |
--------------------------------------------------------------------------------
/docs/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # pyPromptChecker
2 | A small script designed for analyzing AI-generated images created by AUTOMATIC/stable-diffusion-webui.
3 | It extracts incomprehensible strings embedded within image files, formatting them into human-readable.
4 | The formatted data can be exported as JSON and subsequently imported from the JSON output.
5 | Additionally, the script offers in-data search and basic file management functionalities.
6 | All of these features are accessible even without AUTOMATIC/stable-diffusion-webui.
7 |
8 |
9 | # Screenshots
10 | 
11 |
12 | More screenshots [here.](description.md#screenshots)
13 |
14 | # Features
15 |
16 | - Extract creation data, formatting and display it.
17 | - Any number of files can be processed simultaneously (Tested up to 1500 files).
18 | - Image file move and delete with single click.
19 | - JSON export and import.
20 | - Make List of model hash from .safetensors and .ckpt files.
21 | - Tab navigation with filename or thumbnails.
22 | - Search with various conditions.
23 |
24 | See more details [here.](description.md)
25 |
26 |
27 | # Requirements
28 | ### ~~pyPromptChecker binary edition no longer has any requirements.~~
29 | I decided to give up distribute binary editions.
30 | Because very difficult to avoid false positives of Windows Defender.
31 | If you want to continue using you need to install at least python 3.x
32 |
33 | - Python 3.x
34 | - pillow (PIL)
35 | - pypng
36 | - PyQt6
37 | - pyqtdarktheme
38 |
39 | As follows for interrogate features.
40 | - onnxruntime
41 | - numpy
42 | - opencv-python
43 | - huggingface_hub
44 |
45 | # Installation
46 | ### ~~pyPromptChecker binary edition (for Linux and Windows users)~~
47 | ~~1. Download the binary packages from the 'Releases'.~~
48 | ~~2. Extract pyPromptChecker directory to any location of your choice.~~
49 | ~~3. Optionally, desktop files for enable drag-and-drop functionality.~~
50 | ~~4. Execute pyPromptChecker by double-click.~~
51 |
52 | ### pyPromptChecker source code edition
53 |
54 | For Linux
55 | ````bash
56 | git clone https://github.com/yui-tan/pyPromptChecker
57 | cd pyPromptChecker/install
58 | ./install.sh
59 | ````
60 | For Windows
61 | 1. Make sure you've got Python 3.x hanging around.
62 | 2. Download the packages from the 'Releases' or code > download ZIP.
63 | 3. Extract the packages to a location of your choice.
64 | 4. Run command in **PowerShell**.
65 | ```powershell
66 | Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process
67 | cd \path\to\pyPromptChecker\install
68 | ./install.ps1
69 | Set-ExecutionPolicy -ExecutionPolicy Undefined -Scope Process
70 | ```
71 | # Usage
72 | ### Source code edition
73 | ```bash
74 | mikkumiku -a, --ask
75 | # Open directory choose dialog.
76 | mikkumiku -f [FILEPATH ...], --filepath [FILEPATH ...]
77 | # Send file or directory paths to the script.
78 | ```
79 | # Roadmap
80 | - [x] Tab navigation with thumbnails
81 | - [x] Listview
82 | - [x] Diff mode
83 | - [x] Filtering and searching tabs various conditions
84 | - [x] Add support for other image file formats
85 | - [x] jpeg
86 | - [x] webp
87 | - [x] Add support for import JSON
88 | - [ ] Automated LoRa creation
89 | - [x] Positive prompt export to txt files.
90 | - [x] Implement interrogate feature.
91 | - [ ] Import configure from files.
92 | - [ ] Linking sd-scripts.
93 | - [ ] ~~Export Data to Stable-Diffusion-Webui via API~~
94 | - [ ] Get marry to Miku
95 |
96 | # Author
97 | ### Yui-tan / Yuiyui
98 | Nutjob who loves Hatsune Miku.
99 | napier2.718281828@gmail.com
100 | https://civitai.com/user/Yui_tan
101 | https://chichi-pui.com/users/yuiyui20170927
102 |
103 | # Licence
104 | This script created under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html).
105 |
--------------------------------------------------------------------------------
/docs/description.md:
--------------------------------------------------------------------------------
1 | # Supported formats and Webui extensions
2 | - Supported image formats
3 | - png
4 | - jpeg
5 | - webp
6 | - Supported web-ui extension
7 | - Basic information
8 | - Hires.fix
9 | - Extras
10 | - Dynamic thresholding (CFG fix)
11 | - Auto CFG
12 | - CFG scheduling
13 | - Add networks
14 | - Tiled diffusion
15 | - ControlNet
16 | - Regional Prompter
17 |
18 | # Descriptions about features
19 | The authors themselves had no idea it would be so feature-rich.
20 | ## Key bindings
21 | Here are some common key bindings.
22 | The key bindings for each feature, see the feature description.
23 | - ### Basic
24 | - **Ctrl + Tab :** Toggle expand / shorten window
25 | - **Ctrl + D :** Toggle dark/light theme
26 | - **Ctrl + Q :** Quit
27 | - ### Tab control
28 | - **Ctrl + O :** Add tabs
29 | - **Ctrl + N :** Replace tabs
30 | - ### Button control
31 | - **Alt + P :** Copy positive
32 | - **Alt + N :** Copy negative
33 | - **Alt + S :** Copy seed
34 | - **Alt + E :** Open menu
35 | ## Config.ini
36 | - ### Overview
37 | The behaviour of the script is configured in 'config.ini'.
38 | It will work with the default values even without it, but you may want to create your own favourite settings.
39 | The description of each configuration value is given here, but see below for a description of the individual features.
40 | - ### [Window] section
41 | - **"AlwaysOpenBy" option** (Strings)
42 | The strings are case-insensitive.
43 | The valid strings are as follows:
44 | "tab": tab view (default view up to now),
45 | "thumbnail": thumbnail view,
46 | "list": list view,
47 | "random": select view on whim by the script,
48 | "number: select view by file counts.
49 | <11 tab view.
50 | <21 list view.
51 | \>20 thumbnail view.
52 | Default value is **tab**.
53 | - **"AskWhenQuit" option** (Boolean)
54 | Warns when an attempt is made to close the first window that was opened (as it lead to script termination).
55 | Default value is **True**.
56 | - **"AlwaysStartWithDarkMode" option** (Boolean)
57 | If this setting set to True, always turn on dark mode at startup.
58 | Default value is **False**.
59 | - ### [Pixmap] section
60 | - **"PixmapSize" option** (Integer)
61 | Sets the size of the pixmap.
62 | Unlike window size, this one is always adhered to.
63 | The long sides of the image are scaled up/down to this size.
64 | This value has a significant effect on the size of the window itself.
65 | The default value is **350**.
66 | - **"RegionalPrompterPixmapSize" option** (Integer)
67 | Set the size of pixmap of regional prompter tab.
68 | This value also has the same overview as the previous 'PixmapSize' option.
69 | The default value is **500**.
70 | - **"ThumbnailPixmapSize" option** (Integer)
71 | Set the pixmap size of thumbnail mode.
72 | The default value is **150**.
73 | - **"ListViewPixmapSize" option** (Integer)
74 | Set the pixmap size of Listview mode.
75 | The default value is **200**.
76 | - ### [Features] section
77 | - **"SubDirectoryDepth" option** (Integer)
78 | Sets how many levels of subdirectories are searched when a directory path is passed.
79 | The default value is **0**, meaning it will only search for files inside the specified directory.
80 | If set to large value, the script may not function properly due to the large number of files.
81 | The author has confirmed that it can manage up to 10,000 files somehow.
82 | - **"UsesNumberAsTabName" option** (Boolean)
83 | If set to True, the number is used instead of the filename as tab title.
84 | Default value is **False**
85 | - **"OpenWithShortenedWindow" option** (Boolean)
86 | If set to True, the extension tabs at the bottom of the window will start in a hidden state.
87 | Default value is **False**.
88 | - **"ModelListSearchApplyLora" option** (Boolean)
89 | - **"ModelListSearchApplyTi" option** (Boolean)
90 | If this is set to True,
91 | the same process performed on 'Model Hash' will also be applied to LoRa and textual inversion.
92 | In other words, you can freely set the display names for LoRa and textual inversion.
93 | Default value is **False**.
94 |
95 | - ### [Tab] section
96 | - **"HiresExtras" option** (Boolean)
97 | - **"CFG" option** (Boolean)
98 | - **"LoraAddnet" option** (Boolean)
99 | - **"TiledDiffusion" option** (Boolean)
100 | - **"ControlNet" option** (Boolean)
101 | - **"RegionalPrompter" option** (Boolean)
102 | Setting show/hide tab at the bottom of the window.
103 | If set to false, the script don't create tab even if the image uses applicable extension.
104 | Default value is **True** all.
105 | - ### [Ignore] section
106 | - **"IgnoreIfDataIsNotEmbedded" option** (Boolean)
107 | Setting behaviour if image has no embedded data.
108 | If set to True, the script does not create image's tab.
109 | If set to False, it creates image's tab with minimum information.
110 | (e.g. filepath, filename, etc.)
111 | Default value is **False**.
112 | ## Model_list.csv
113 | - ### Overview
114 | 'model_list.csv' is a file what script locate model name from hash values.
115 | This file can be place anywhere of your choice.
116 | But if you move from initial place, declare new path in 'config.ini'
117 | And you **can not** rename this file from 'model_list.csv'
118 |
119 | - ### Format
120 | The structure of 'model_list.csv' is as following:
121 |
122 | | Display name | Model hash | Entire SHA256 hash | Filename | Model type |
123 | |:------------:|:----------:|:------------------:|:--------:|:----------:|
124 |
125 | - The display name is the same as the filename if freshly output from the script.
126 | - But there is no issue to edit it according to your preference.
127 | - Each of value must be comma-separated.
128 | - And the values must not be enclosed in quotation marks.
129 | - The script uses the first and second columns for searching.
130 | - Therefore, you can delete columns from the third onward or add something in columns from the sixth onward without any issues.
131 |
132 | - ### Model hash extractor
133 | The feature that extract model hash from your own model files.
134 | And create 'model_list.csv' file or append data to it.
135 | Now it can be find in menu > model hash extractor.
136 | Depends on number of files, it requires huge mount of time and memories[^2].
137 |
138 |
139 | - ### Related values in 'config.ini'
140 | - ### [Location] section ###
141 | - **"ModelList" option** (Directory path)
142 | If this value is unset, the program will search in the same directory as 'config.ini' file by default.
143 | If you relocate 'model_list.csv', **you must** declare the new path by setting this parameter.
144 |
145 | ## JSON import and export
146 | - ### Overview
147 | Export creation data as JSON formatted data.
148 | It can now be found in the menu > Export JSON
149 | 'Import JSON' feature is not intended to import any JSON other than what it has exported[^1].
150 | If you use 'import JSON' feature, all tabs will be replaced.
151 |
152 | - ### Key bindings
153 | - **Alt + T :** Export JSON of present image file
154 | - **Alt + A :** Export JSON of all image file
155 | - **Alt + L :** Export JSON of selected image file
156 |
157 | - ### Related values in 'config.ini'
158 | - ### [Features] section ###
159 | - **"JsonSingle" option** (Strings)
160 | This option is setting for exported JSONs (single file) default Name.
161 | Setting whatever you want.
162 | If 'JsonSingle=filename' is set, the image file name will be set as default.
163 | - **"JsonMultiple" option** (Strings)
164 | This option is setting for exported JSONs (all file) default Name.
165 | Setting whatever you want.
166 | If 'JsonMultiple=directory' is set, the first image's directory name will be set as default.
167 | - **"JsonSelected" option** (Strings)
168 | This option is setting for exported JSONs (selected) default Name. Setting whatever you want.
169 | If 'JsonSelected=selected' is set, the first image's name + "-and-so-on" will be set as default. maybe...
170 |
171 | ## Move/Delete feature
172 | - ### Overview
173 | Provides simple file management functions.
174 | Images can be moved to a pre-registered favourite directory,
175 | Moved to an arbitrary directory and can be deleted.
176 | If enable this feature, three buttons appear beneath pixmap.
177 | - Favourite button: Move/copy image to favourite directory.
178 | - Move to button: Move/copy image to any directory.
179 | - Delete button: Delete[^3] image file.
180 |
181 | - ### Key bindings
182 | - **Alt + F :** Add favourite
183 | - **Alt + M :** Move to
184 | - **Delete :** Delete image
185 |
186 | - ### Related values in 'config.ini'
187 | - ### [Location] section ###
188 | - **"Favourites" option** (Directory path)
189 | Set an absolute directory path here to gather your favourite images.
190 | However, if you leave this value blank or enter a non-existent directory path,
191 | the favourite button will still appear, but this features won't be available.
192 | - ### [Features] section ###
193 | - **"MoveDelete" option** (Boolean)
194 | If set to True, 3 buttons will be shown beneath the image.
195 | Default value is **True**.
196 | - **"UseCopyInsteadOfMove" option** (Boolean)
197 | If set to True, this scripts will copy file instead of moving it.
198 | Default value is **True**.
199 | *But this setting is not affect to 'Delete' feature.*
200 | - **"AskIfDelete" option** (Boolean)
201 | If set to True, confirmation dialog will be shown when the delete button is pressed.
202 | Default value is **True**.
203 | - **"AskIfClearTrashBin" option[^4]** (Boolean)
204 | If set to True, confirmation dialog will be shown the script exits,
205 | Asking if delete all files within the TrashBin directory.
206 | Default value is **True**.
207 | ## Tab navigation ##
208 | 
209 | - ### Overview ###
210 | This is provided to allow navigation between tabs when many tabs are generated.
211 | A combo box at the top of the window and a thumbnail window are provided.
212 | - ### Key bindings
213 | - **Ctrl + F :** Search
214 | - **Ctrl + T :** Open thumbnail window
215 | - **Ctrl + L :** Open listview window
216 | - ### Related values in 'config.ini' ###
217 | - ### [Features] section ###
218 | - **"TabNavigation" option** (Boolean)
219 | Toggle to enable or to disable tab navigation.
220 | Default value is **True**.
221 | - **"TabNavigationMinimumTabs" option** (Integer)
222 | Setting appear tab navigation when how many files are opens.
223 | This setting is meaningless unless both of the above settings are enabled.
224 | Default value is **2**
225 | ## Thumbnail tab bar
226 | - ### Overview
227 | Replace the bland tab bar that only shows numbers or filenames by thumbnails.
228 | Keep in mind that this may significantly change the width or height of the window.
229 | - ### Key bindings
230 | - **Ctrl + B :** Show / hide thumbnail tab bar
231 | - ### Related values in 'config.ini'
232 | - ### [Features] section
233 | - **"ThumbnailTabBar" option** (Boolean)
234 | Toggle to enable or to disable thumbnail tab bar.
235 | Default value is **False**.
236 | - **"ThumbnailTabBarVertical" option** (Boolean)
237 | The thumbnail tab bar is shown vertically on the right side of the window.
238 | But if set to False, it is displayed horizontally at the top of the window.
239 | However, this feature is experimental in v2.1.0.
240 | Default value is **True**.
241 | - **"HideNormalTabBar" option** (Boolean)
242 | If set to True, hide the normal tab bar and replace altogether.
243 | Default value is **False**.
244 | ## Tab search
245 | 
246 | - ### Overview ###
247 | Search loaded image files using specified criteria.
248 | For now, search using following conditions.
249 | - Text string in positive prompt, negative prompt and region control.
250 | - Model name
251 | - Seed
252 | - CFG (less than, equal to, greater than)
253 | - Whether extensions are used or not.
254 | - ### Search conditions
255 | - **Words separated by spaces are treated as AND conditions.**
256 | e.g. [hatsune miku] is treated as [hatsune AND miku]
257 | Matches [hatsune miku], [hatsune -the goddess of sound- miku] and [miku hatsune]
258 | - **Words split by | are treated as OR conditions.**
259 | e.g. [kagamine rin|len] is treated as [kagamine AND (rin OR len)]
260 | Matches [kagamine rin, kagamine len], [kagamine rin] and [kagamine len]
261 | - **Sentences enclosed in double quotes are treated as phrases.**
262 | e.g. ["megurine luka"] doesn't match [megurine the pink hair luka] and [luka megurine], only matches [megurine luka]
263 | - **Model name, seed, cfg and extension conditions are all treated as AND conditions.**
264 | - Above images means:
265 | "hatsune miku" AND (sitting OR reclining) AND "bikini" in (Positive prompt OR Region control)
266 | AND AbyssOrangeMix2 hard used
267 | AND CFG is greater than 8.0
268 | AND Tiled diffusion used
269 | - ### Related values in 'config.ini' ###
270 | - ### [Features] section ###
271 | - **"TabSearch" option** (Boolean)
272 | This value toggles enables / disables the tab search features.
273 | If set to True, appears 'Search' and 'Restore' buttons.
274 | And tab search feature to be enabled.
275 | Default value is **True**.
276 | - **"HideNotMatchedTabs" option** (Boolean)
277 | When search results are shown in tabs, hide the tabs that were not match by the search.
278 | If set to True, hide the tab.
279 | If set to False, only change the tab title colour to green.
280 |
281 | ## Compare extension
282 | 
283 | - ### Overview ###
284 | If left click on bottom tab (e.g. Prompt, Tiled Diffusion, etc) appears menu with checkbox.
285 | Check the checkbox to maintain the selected tab in the bottom,
286 | even when you switch tabs in the top.
287 | # Screenshots
288 | - ### Main screen
289 | 
290 | 
291 | - ### Highres fix and Extras
292 | 
293 | - ### Lora and Add network
294 | 
295 | - ### CFG
296 | 
297 | - ### Tiled diffusion
298 | 
299 | - ### Controlnet
300 | 
301 | - ### Regional prompter
302 | 
303 | 
304 | - ### Region control in Tiled diffusion
305 | - ### Image view
306 | 
307 | - ### Thumbnail tab navigation
308 | 
309 |
310 | [^1]:But it would be interesting to try it out.
311 | [^2]:It may require more than 32 GiB of memory (not VRAM).
312 | For example, in my case, it took 20 minutes to process 62 files, and the memory usage went up to 29 GiB.
313 | [^3]:Pressing the delete button will not actually perform the deletion:instead, the file will be moved to TrashBin directory (/pyPromptChecker/.trash).
314 | The actual deletion of files occurs when the script exits.
315 | [^4]:For fail-safe, it is highly recommended to set 'Ture' for 'AskIfClearTrashBin'.
316 | It might be a bit bothersome but even if the script crashes, image files will remain in '/pyPromptChecker/.trash'.
317 | Additionally, even if you accidentally delete image files, the script should protect your files if you press 'cancel'
318 |
--------------------------------------------------------------------------------
/install/ebuild/media-gfx/pyPromptChecker/pyPromptChecker-1.4.0.ebuild:
--------------------------------------------------------------------------------
1 | # Copyright 1999-2023 Gentoo Authors
2 | # Distributed under the terms of the GNU General Public License v2
3 |
4 | EAPI=8
5 | DISTUTILS_USE_PEP517=setuptools
6 | PYTHON_COMPAT=( python3_{10..12} )
7 |
8 | inherit distutils-r1
9 |
10 | DESCRIPTION="This is a script for AI-generated image."
11 | HOMEPAGE="https://github.com/yui-tan/pyPromptChecker"
12 | SRC_URI="https://github.com/yui-tan/pyPromptChecker/archive/refs/tags/v${PV}.tar.gz"
13 |
14 | LICENSE="GPL-3"
15 | SLOT="0"
16 | KEYWORDS="amd64"
17 | IUSE=""
18 |
19 | RDEPEND="
20 | dev-python/pypng
21 | dev-python/pillow
22 | dev-python/PyQt6
23 | "
24 | DEPEND="${RDEPEND}"
25 | BDEPEND=""
26 |
27 | python_install() {
28 | distutils-r1_python_install
29 | }
30 |
--------------------------------------------------------------------------------
/install/ebuild/media-gfx/pyPromptChecker/pyPromptChecker-9999.ebuild:
--------------------------------------------------------------------------------
1 | # Copyright 1999-2023 Gentoo Authors
2 | # Distributed under the terms of the GNU General Public License v2
3 |
4 | EAPI=8
5 | DISTUTILS_USE_PEP517=setuptools
6 | PYTHON_COMPAT=( python3_{10..12} )
7 |
8 | inherit distutils-r1 git-r3
9 |
10 | DESCRIPTION="This is a script for AI-generated image."
11 | HOMEPAGE="https://github.com/yui-tan/pyPromptChecker"
12 | EGIT_REPO_URI="https://github.com/yui-tan/pyPromptChecker"
13 |
14 | LICENSE="GPL-3"
15 | SLOT="0/9999"
16 | KEYWORDS="~amd64"
17 | IUSE=""
18 |
19 | RDEPEND="
20 | dev-python/pypng
21 | dev-python/pillow
22 | dev-python/PyQt6
23 | "
24 | DEPEND="${RDEPEND}"
25 | BDEPEND=""
26 |
27 | src_unpack() {
28 | git-r3_src_unpack
29 | }
30 |
31 | python_install() {
32 | distutils-r1_python_install
33 | }
--------------------------------------------------------------------------------
/install/install.ps1:
--------------------------------------------------------------------------------
1 | $directory = (Get-Item -Path $PSScriptRoot).FullName
2 | $targetPath = (Split-Path -Path $directory)
3 | $executePath = Join-Path $targetPath "venv\Scripts\pythonw.exe"
4 | $packagePath = Join-Path $targetPath "pyPromptChecker"
5 | $shortcutPath = Join-Path $targetPath "pyPromptChecker.lnk"
6 | $commandPath = Join-Path $targetPath "pyPromptChecker\main.py"
7 | $iconPath = Join-Path $targetPath "pyPromptChecker\icon\icon.ico"
8 | $python = (python --version 2>&1)
9 |
10 | Set-Location $targetPath
11 |
12 | if (!($python -like 'Python 3.*')) {
13 | Write-Host "Python 3.x is not installed. Please install Python 3.x before running this script."
14 | Start-Process "https://www.python.org/"
15 | exit 1
16 | }
17 |
18 | if (!(Test-Path -Path ".\venv" -PathType Container)) {
19 | python -m pip install --upgrade pip
20 | Write-Host "Installing venv..."
21 | python -m venv venv
22 | Write-Host "Activating venv..."
23 | . .\venv\Scripts\Activate
24 | Write-Host "Installing pyPromptChecker..."
25 | python -m pip install --upgrade pip
26 | pip install -e .
27 | Write-Host "Instalation has been done."
28 | }
29 |
30 | if (!(Test-Path -Path $shortcutPath -PathType Leaf)) {
31 | Write-Host "Shortcut creating..."
32 | $WshShell = New-Object -ComObject WScript.Shell
33 | $shortcut = $WshShell.CreateShortcut($shortcutPath)
34 | $shortcut.TargetPath = $executePath
35 | $shortcut.Arguments = $commandPath
36 | $shortcut.IconLocation = $iconPath
37 | $shortcut.WorkingDirectory = $packagePath
38 | $shortcut.Save()
39 | Write-Host "Shortcut created at $shortcutPath"
40 | }
41 |
42 | Write-Host "Everything has been done!"
43 | exit 0
44 |
--------------------------------------------------------------------------------
/install/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Check if the current directory is "install"
4 | if [[ $(basename $(pwd)) != "install" ]]; then
5 | echo "This script should be executed in the 'pyPromptChecker/install' directory."
6 | exit 1
7 | fi
8 |
9 | cd ..
10 |
11 | # Check if the current directory is "pyPromptChecker"
12 | if [[ $(basename $(pwd)) != "pyPromptChecker" ]]; then
13 | echo "This script should be executed in the 'pyPromptChecker/install' directory."
14 | exit 1
15 | fi
16 |
17 | # Check if Python 3 is installed
18 | if ! command -v python3 &> /dev/null; then
19 | echo "Python 3 is not installed."
20 | exit 1
21 | fi
22 |
23 | # Create and activate the virtual environment
24 | if [[ ! -d "venv" ]]; then
25 | echo "Installing venv..."
26 | python3 -m venv venv
27 | fi
28 |
29 | echo "activating venv..."
30 | source venv/bin/activate
31 |
32 | pip install --upgrade pip
33 |
34 | # Install pyPromptChecker into venv
35 | echo "Installing pyPromptChecker..."
36 | pip3 install -e.
37 |
38 | # Making .sh file and .desktop file
39 | echo "Making files..."
40 | mkdir sh
41 |
42 | path_to_venv=$(pwd)"/venv/bin/activate"
43 | path_to_icon=$(pwd)"/pyPromptChecker/icon/icon.png"
44 | command[0]="/bin/bash -c 'source $path_to_venv && mikkumiku \"\$@\"' bash"
45 | command[1]="/bin/bash -c \"source $path_to_venv && mikkumiku --ask\""
46 | command_name[0]="mikkumiku"
47 | command_name[1]="folder_picker"
48 |
49 | for i in 0 1
50 | do
51 | {
52 | echo -e "[Desktop Entry]"
53 | echo -e "Name=pyPromptChecker"
54 | echo -e "Exec=${command[i]}"
55 | echo -e "Path=$(pwd)"
56 | echo -e "Comment=A tiny script for AI images"
57 | echo -e "Terminal=false"
58 | echo -e "Icon=${path_to_icon}"
59 | echo -e "Type=Application"
60 | } >> sh/${command_name[i]}.desktop
61 | done
62 |
63 | # Explanation
64 | echo "Now Installation is finished."
65 | echo "In a new directory 'PyPromptChecker/sh', two files have been created."
66 | echo "The following are the two files:"
67 | echo
68 | echo "mikkumiku.desktop:"
69 | echo "When you run this file, a file selection dialog will appear,"
70 | echo "and once a file is selected, processing will begin."
71 | echo "This is same as running command 'mikkumiku'"
72 | echo
73 | echo "folder_picker.desktop:"
74 | echo "When you run this file, a directory selection dialog will appear,"
75 | echo "and once a directory is selected, processing will begin."
76 | echo "This is same as running command 'mikkumiku --ask'"
77 | echo
78 |
79 | exit 0
--------------------------------------------------------------------------------
/model_list.csv:
--------------------------------------------------------------------------------
1 | Dispaly_name,Model_hash,SHA256,Filename,Type
2 | MeinaMix V10,77b7dc4ef0,77b7dc4ef0fca64ed8c3c38e80efa3e559488ee377f4138ec4bce488ef6c8412,meinamix_meinaV10,safetensors
3 | Waifu Diffusion 1.4 Anime,e8f4b2225b,e8f4b2225b8ac6464e9d13d25b708349b1ca340c92dfc3575e8d5ef18f689457,wd-1-4-anime_e1,ckpt
4 | FaceBombMix V1,7364c31aac,7364c31aac1c3afbad151f719fea246d6016c6ad08d27c5666fb2c686f5c6113,facebombmix_v1Bakedvae,safetensors
5 | f111,6138f9c13c,6138f9c13c4caaaead14ac421a8c688f84a4783ec5ee4c13b91833beb8f17c3c,f111,ckpt
6 | CetusMix V3.5,a611cf9c19,a611cf9c19e8f8011a0912cb064b8b71a542d834422e9da53420eac87db38bd8,cetusMix_Version35,safetensors
7 | MeinaHentai V3,7c01e48033,7c01e4803344e6888bd72fa4f4c7d33a7270a977fee9cb0ca2ea7a96530e8884,meinahentai_v3,safetensors
8 | ReV Animated V1.2.2,4199bcdd14,4199bcdd147e11328d5f3560301d5a7ab4ac7eeefbf49dc3eb663cb3e772b9ac,revAnimated_v122,safetensors
9 | Perfect World V4,24a393500f,24a393500f15c3243a4212c2ceab764e43f343d8442b0f4cec430dac6ea00ecb,perfectWorld_v4Baked,safetensors
10 | AnythingGape-fp16,1bb0969a92,1bb0969a9236a9e105393f840c695598dc27a888c83a3c9debf5d9225cfc02fa,AnythingGape-fp16,ckpt
11 | DreamShaper 6,b76cc78ad9,b76cc78ad9e2f001603f200d4e26153ce565a6ac3c179ada73d2e4a4071d4eac,dreamshaper_6BakedVae,safetensors
12 | GhostMix V2.0,e3edb8a26f,e3edb8a26f44dfddc124129004b5ded3378be974b8d3bf82f43b9660d416d3d1,ghostmix_v20Bakedvae,safetensors
13 | Any Hentai V2.0,61bc7001e8,61bc7001e86117c96a1de88c338bb07daa5bafef0cbe355ee40c6228a21ead12,anyhentai_20,safetensors
14 | PerfectWorld V3,0f49d1caa2,0f49d1caa209f0361165def825bedc6a7a2b3d1f4ada14af992a61bd37feb6e4,perfectWorld_v3Baked,safetensors
15 | Stable Diffusion v1.4,fe4efff1e1,fe4efff1e174c627256e44ec2991ba279b3816e364b49f9be2abc0b3ff3f8556,sd-v1-4,ckpt
16 | NAI Diffusion Leaked,89d59c3dde,89d59c3dde4c56c6d5c41da34cc55ce479d93b4007046980934b14db71bdb2a8,model,ckpt
17 | PerfectWorld V2,79e42fb744,79e42fb7445bb08cb16e92cfd57f3ab09b57f18b1b8bcb27cb5d5d4e19ac1eec,perfectWorld_v2Baked,safetensors
18 | bp_mk5,f87dabceff,f87dabceffd299a8f3f72f031829338e34ad3c1e2541815af08fa694d65fb4c0,bp_mk5,safetensors
19 | Anything V3,10f0bd7ade,10f0bd7adec2038b7652874b54ec05cd7d8d699c9bfe048825dff840145c9102,Anything-V3.0,safetensors
20 | AbyssOrangeMix3,d124fc18f0,d124fc18f0232d7f0a2a70358cdb1288af9e1ee8596200f50f0936be59514f6d,AOM3,safetensors
21 | Waifu Diffusion 1.5 Beta 2,764f93581d,764f93581d80b46011039bb388e899f17f7869fce7e7928b060e9a5574bd8f84,wd-1-5-beta2-fp32,safetensors
22 | Waifu Diffusion 1.3,a8941cf7bf,a8941cf7bf04f5685af5c54963a3c6b755bfdf12884aedfd1b0c228cd5ae127d,wd-v1-3-full,ckpt
23 | instagram-latest-plus-clip-v6e1_50000,8f1d325b19,8f1d325b194570754c6bd06cf1e90aa9219a7e732eb3d488fb52157e9451a2a5,instagram-latest-plus-clip-v6e1_50000,safetensors
24 | gape60,17a40d7de3,17a40d7de3ceb5e138e003e2a0b5b313d2db2d61be7c918b394a90788a1ea775,gape60,ckpt
25 | 3DKX_1.0b,1d45c7c094,1d45c7c094cf7d75b6703c503cea35dbbb3f1f034c55d6b062abf94ed3b58f8d,3DKX_1.0b,safetensors
26 | Anything V3-pruned-fp32,875546ff2e,875546ff2e9b953541de94de079424c1771ca9f9dc89064735fea3d87141aabe,Anything-V3.0-pruned-fp32,safetensors
27 | last-pruned,54faf6de03,54faf6de03fb9a6fe4d8af163b16133cd7cd045d45915178c602e4b51a92036c,last-pruned,ckpt
28 | HD-22,a05d076a39,a05d076a39d8e613d8d24659cb859e9afc2b95cabf80ec1fe015124078d4ff29,HD-22,ckpt
29 | BasilMix-fixed,0ff127093f,0ff127093f5be455057742c40cef578407b6933f240ee8dc5ed0f3061196fb38,Basil_mix_fixed,safetensors
30 | ChilloutMix Ni-Pruned-Fp32-Fix,fc2511737a,fc2511737a54c5e80b89ab03e0ab4b98d051ab187f92860f3cd664dc9d08b271,chilloutmix_NiPrunedFp32Fix,safetensors
31 | derrida_final,42d3f359b0,42d3f359b02c944cb2e8f666b10bda05015792f10bfd4e9deee44bb66969ce3d,derrida_final,ckpt
32 | majicMIX realistic V5,33c9f6dfcb,33c9f6dfcbd3b86e76ffdfc58253ee73f6c1b513238481dcd5d6a27b7ad3df0f,majicmixRealistic_v5,safetensors
33 | Elysium V1,abbb28cb5e,abbb28cb5e70d3e0a635f241b8d61cefe42eb8f1be91fd1168bc3e52b0f09ae4,Elysium_V1,ckpt
34 | Uber Realistic Porn Merge V1.3,40f9701da0,40f9701da0953f5215fb45f801ba61afc94e39a5f5d0d01a2bca2d41ab219b78,uberRealisticPornMerge_urpmv13,safetensors
35 | AbyssOrangeMix3 A2,553398964f,553398964f9277a104da840a930794ac5634fc442e6791e5d7e72b82b3bb88c3,AOM3A2,safetensors
36 | Hassaku (hentai model) V1,df614cd3c2,df614cd3c2a592e5e6eb4e8dfafe83c58de750781b6390bdd6b449b73315ce4b,hassakuHentaiModel_hassakuv1,safetensors
37 | f222,f300684443,f300684443092d39cd717c92ae19836114960a560dabb887d2fca370e2cc2531,f222,safetensors
38 | AbyssOrangeMix2 Hard,0fc198c490,0fc198c4908e98d7aae2a76bd78fa004e9c21cb0be7582e36008b4941169f18e,AbyssOrangeMix2_hard,safetensors
39 | trinart_characters_it4_v1,d58058f2c7,d58058f2c71612aa7492d63ad8e6c32b3159494dc51a29ffe71937cdc49b6681,trinart_characters_it4_v1,ckpt
40 | evt_v2-ema-pruned,39230a52bc,39230a52bcf51cb9768f21c83e0a7478351e9add7c9294bfc17ba36ecc85d71c,evt_v2-ema-pruned,ckpt
41 | AbyssOrangeMix3 A1B,5493a0ec49,5493a0ec491f5961dbdc1c861404088a6ae9bd4007f6a3a7c5dee8789cdc1361,AOM3A1B,safetensors
42 | gape22_yiffy15,aea020931f,aea020931f4db09dd459db495cc8b1c81e415430e756eda3dbba38b08da77fa0,gape22_yiffy15,ckpt
43 | Anything V3-pruned,6806d4c0f8,6806d4c0f86a2f39d60a97bcf926f39d8b8fce2c71e39baf4ef0ee40a5655632,Anything-V3.0-pruned,safetensors
44 | bp_1024_with_vae_te,d5d6e1898f,d5d6e1898fbc096237038661a5a09bdd25ea31f7a942b0df414d4bae36e9a124,bp_1024_with_vae_te,ckpt
45 | bp_mk3,97848d7d80,97848d7d80b242a1483d0307509b422fee12a0e7096ff202397a6e395a71aea9,bp_mk3,safetensors
46 | Anything V3-pruned-fp16,ed376204fb,ed376204fb1e93cde85757de92c9f737dfb53cef97e7fadb33b99f94e2423469,Anything-V3.0-pruned-fp16,safetensors
47 | AbyssOrangeMix3 A3,eb4099ba9c,eb4099ba9cd5e69ab526fca22a2e967f286f8512d9509b735c892fa6468767cf,AOM3A3,safetensors
48 | URPM-OrangeHard-chilloutNi v2.0,5363c6dd80,5363c6dd805045dfc07103f05668b24bea388ce19280db92270acfb438deff87,urpmOrangehard_v20,safetensors
49 | gg1342_testrun1_pruned,ebe2f8dfee,ebe2f8dfeed0b87080a37e94bf0aca8800fc10691826a5e76259faf509106246,gg1342_testrun1_pruned,ckpt
50 | Anything V4.5,1d1e459f9f,1d1e459f9f549a404746390de21df33b8c8134f863b3b5da7d784843e782900a,anything-v4.5,safetensors
51 | 3DKX_v1.1,998f6b580e,998f6b580e656a1139205a09ccb0ad282765525a38b0d690fc4fb50a249057af,3DKX_v1.1,safetensors
52 | Evt_V3_ema,af99db519a,af99db519a64052f13daf3e714ab0298fb74f78d552c6acca487a5463d9152d7,Evt_V3_ema,ckpt
53 | AbyssOrangeMix,6bb3a5a3b1,6bb3a5a3b1eadd32dfbc8f0987559c48cb4177aee7582baa6d6a25181929b345,AbyssOrangeMix,safetensors
54 | Elysium Anime V3,50265e48df,50265e48dfa0b53c6fa242e66c10106c85d1d68f1f188109475f6b70043d5497,Elysium_Anime_V3,safetensors
55 | bp_1024_e10_ema,b62854e4c4,b62854e4c4817c2b515e1b1a13b1ec8498ba71fdbd492496f021a4d257698d0b,bp_1024_e10_ema,ckpt
56 | AbyssOrangeMix3 A1,f303d10812,f303d108122ddd43a34c160bd46dbb08cb0e088e979acda0bf168a7a1f5820e0,AOM3A1,safetensors
57 | ACertainty,a64573359a,a64573359af0f1071ef01d0dc93df2bc90eb1d0bcf3e26058fbf5aeff37c6462,ACertainty,ckpt
58 | Waifu Diffusion 1.5 Beta 2 Aesthetic,711cd95c77,711cd95c77dc04e4f76a40c06b4c8fce64c1c4373c7d461a9121cb624afe6dcd,wd-1-5-beta2-aesthetic-fp32,safetensors
59 | r34_e4,182b9a6201,182b9a620161dcaed6790c3575abdda32570ac314e76e04fe3154637059a739b,r34_e4,ckpt
60 | AnythingV3 pruned-fp16,38c1ebe3,,Anything-V3.0-pruned-fp16.ckpt,ckpt
61 | Anything-V3.0.vae,f921fb3f2989,f921fb3f29891d2a77a6571e56b8b5052420d2884129517a333c60b1b4816cdf,Anything-V3.0.vae,pt
62 | ClearVAE_V2.2,54b156d6ce34,54b156d6ce34d0627ca0b63a824f58f5bf9c4e879549eb84ec499662726c4013,ClearVAE_V2.2,safetensors
63 | HD-22.vae,f921fb3f2989,f921fb3f29891d2a77a6571e56b8b5052420d2884129517a333c60b1b4816cdf,HD-22.vae,pt
64 | autoencoder_kl-f8-trinart_characters.vae,d2dd1c82220e,d2dd1c82220e31a72bd9958dda249ed7f94faf875d5123ae3aab7a1950a82a8f,autoencoder_kl-f8-trinart_characters.vae,pt
65 | clearvae_main,600345c50378,600345c503784cd77536d714f0e4c43f9e1fa4379007e730d54c454c66ee36db,clearvae_main,safetensors
66 | kl-f8-anime.vae,2f11c4a99ddc,2f11c4a99ddc28d0ad8bce0acc38bed310b45d38a3fe4bb367dc30f3ef1a4868,kl-f8-anime.vae,pt
67 | kl-f8-anime2.vae,df3c506e51b7,df3c506e51b7ee1d7b5a6a2bb7142d47d488743c96aa778afb0f53a2cdc2d38d,kl-f8-anime2.vae,pt
68 | model.vae,f921fb3f2989,f921fb3f29891d2a77a6571e56b8b5052420d2884129517a333c60b1b4816cdf,model.vae,pt
69 | sdxl_vae,63aeecb90ff7,63aeecb90ff7bc1c115395962d3e803571385b61938377bc7089b36e81e92e2e,sdxl_vae,safetensors
70 | vae-ft-mse-840000-ema-pruned.vae,c6a580b13a5b,c6a580b13a5bc05a5e16e4dbb80608ff2ec251a162311590c1f34c013d7f3dab,vae-ft-mse-840000-ema-pruned.vae,pt
71 |
--------------------------------------------------------------------------------
/pyPromptChecker/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yui-tan/pyPromptChecker/8a93d098df0888d81afcbf67e8a3ec6851cd9e5b/pyPromptChecker/__init__.py
--------------------------------------------------------------------------------
/pyPromptChecker/gui/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | import configparser
5 |
6 | config = {}
7 |
8 | module_filepath = os.path.abspath(os.path.dirname(__file__))
9 | package_filepath = os.path.dirname(module_filepath)
10 | installed_filepath = os.path.dirname(package_filepath)
11 | config_file = os.path.join(installed_filepath, 'config.ini')
12 |
13 | ini_config = configparser.ConfigParser()
14 | ini_config.read(config_file, encoding='utf-8')
15 | ini_section = [['Location', 'ModelList', 'Favourites'],
16 | ['Window', 'MaxWindowWidth', 'MaxWindowHeight', 'AlwaysStartWithDarkMode', 'AlwaysOpenBy'],
17 | ['Pixmap', 'PixmapSize', 'RegionalPrompterPixmapSize', 'ThumbnailPixmapSize', 'ListViewPixmapSize'],
18 | ['Features', 'SubDirectoryDepth', 'OpenWithShortenedWindow', 'UsesNumberAsTabName'],
19 | ['Features', 'ModelListSearchApplyLora', 'ModelListSearchApplyTi'],
20 | ['Features', 'JsonExport', 'JsonSingle', 'JsonMultiple', 'JsonSelected'],
21 | ['Features', 'MoveDelete', 'UseCopyInsteadOfMove', 'AskIfDelete', 'AskIfClearTrashBin'],
22 | ['Features', 'TabNavigation', 'TabNavigationWithThumbnails', 'TabNavigationWithListview'],
23 | ['Features', 'TabNavigationMinimumTabs', 'ThumbnailTabBar', 'ThumbnailTabBarVertical', 'HideNormalTabBar'],
24 | ['Features', 'TabSearch', 'HideNotMatchedTabs'],
25 | ['Tab', 'HiresExtras', 'CFG', 'LoraAddNet', 'TiledDiffusion', 'ControlNet', 'RegionalPrompter'],
26 | ['Ignore', 'IgnoreIfDataIsNotEmbedded'],
27 | ['Debug', 'ErrorList'],
28 | ]
29 |
30 | for ini in ini_section:
31 | section = ini[0]
32 | for option in ini[1:]:
33 | if ini_config.has_option(section, option):
34 | if section == 'Location':
35 | value = ini_config[section].get(option)
36 | if '\\' in value:
37 | value = value.replace('\\\\', '\\')
38 | if os.path.exists(value):
39 | config[option] = value
40 | elif section == 'Window' and option == 'AlwaysOpenBy':
41 | value = ini_config[section].get(option)
42 | config[option] = value
43 | elif (section == 'Window' and not option == 'AlwaysStartWithDarkMode') or section == 'Pixmap':
44 | try:
45 | value = ini_config[section].getint(option)
46 | except ValueError:
47 | continue
48 | if section == 'Window' and value > 479:
49 | config[option] = value
50 | elif section == 'Pixmap' and 99 < value < 801:
51 | config[option] = value
52 | elif section == 'Debug':
53 | value = ini_config[section].get(option)
54 | if value == 'AlwaysOff':
55 | int_value = 0
56 | elif value == 'AlwaysOn':
57 | int_value = 2
58 | else:
59 | int_value = 1
60 | config[option] = int_value
61 | elif section == 'PNG':
62 | if ini_config.has_option(section, 'Accept'):
63 | try:
64 | value = ini_config[section].getint(option)
65 | except ValueError:
66 | continue
67 | config[option] = value
68 | elif option == "JsonSingle" or option == 'JsonMultiple' or option == 'JsonSelected':
69 | value = ini_config[section].get(option)
70 | if value:
71 | config[option] = value
72 | elif option == 'TabNavigationMinimumTabs' or option == 'SubDirectoryDepth':
73 | try:
74 | value = ini_config[section].getint(option)
75 | except ValueError:
76 | continue
77 | config[option] = value
78 | else:
79 | try:
80 | value = ini_config[section].getboolean(option)
81 | except ValueError:
82 | continue
83 | config[option] = value
84 |
85 | if not config.get('ModelList'):
86 | config['ModelList'] = os.path.join(installed_filepath, 'model_list.csv')
87 |
88 | estimated_icon_path = os.path.abspath(os.path.join(package_filepath, 'icon/icon.png'))
89 | config['IconPath'] = estimated_icon_path
90 | config['Installed'] = installed_filepath
91 |
--------------------------------------------------------------------------------
/pyPromptChecker/gui/custom.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | from PyQt6.QtGui import QKeySequence, QShortcut, QColor
5 |
6 | from . import config
7 |
8 |
9 | def custom_stylesheet(category: str, purpose: str):
10 | if category == 'groupbox':
11 | style = 'QGroupBox {border: 2px solid @@@ ; padding : 1px 0 0 0; }'
12 | return style.replace('@@@', custom_color(purpose))
13 | elif category == 'colour':
14 | style = 'color: @@@;'
15 | return style.replace('@@@', custom_color(purpose))
16 | elif category == 'border':
17 | style = 'border: 2px solid @@@'
18 | return style.replace('@@@', custom_color(purpose))
19 | elif category == 'pixmap':
20 | style = 'border: 1px solid @@@'
21 | return style.replace('@@@', custom_color(purpose))
22 | elif category == 'label':
23 | style = 'border-top: 2px solid transparent; border-bottom: 2px solid @@@ ; border-radius: 0px'
24 | return style.replace('@@@', custom_color(purpose))
25 | elif category == 'extension_label':
26 | style = 'border-radius: 5px ; border: 2px solid @@@ ; background-color: @@@ ; color: white ;'
27 | return style.replace('@@@', custom_color(purpose))
28 | elif category == 'extension_label_disable':
29 | return 'border-radius: 5px ; border: 1px solid palette(shadow);'
30 | elif category == 'title':
31 | style = 'QGroupBox::title {color: @@@; }'
32 | return style.replace('@@@', custom_color(purpose))
33 | elif category == 'slider':
34 | return custom_color(purpose)
35 | elif category == 'theme':
36 | if purpose == 'dark':
37 | return ("QPushButton { color: rgba(134, 206, 203, 1.0); } "
38 | "QPushButton:hover { background:rgba(134, 206, 203, 0.110) } "
39 | "QPushButton:default { background: rgba(134, 206, 203, 1.0); } "
40 | "QPushButton:default:hover {background: rgba(134, 206, 203, 1.0); } "
41 | "QPushButton:default:pressed,QPushButton:default:checked {background: rgba(134, 206, 203, 1.0); } "
42 | "QLabel { selection-background-color: rgba(19, 122, 127, 1.0); } "
43 | "QTextEdit:focus, QTextEdit:selected, QLineEdit:focus, QLineEdit:selected { selection-background-color: rgba(19, 122, 127, 1.0); } "
44 | "QTextEdit:focus, QLineEdit:focus { border-color: rgba(134, 206, 203, 1.0); } "
45 | "QSplitter:handle:hover { background-color: rgba(134, 206, 203, 1.0); } "
46 | "QTabBar:tab:selected:enabled { color: rgba(134, 206, 203, 1.0); border-color: rgba(134, 206, 203, 1.0); } "
47 | "QProgressBar::chunk {background: rgba(134, 206, 203, 1.0); } "
48 | "QCheckBox:hover,QRadioButton:hover {border-bottom:2px solid rgba(134, 206, 203, 1.0); }"
49 | "QSlider::sub-page:horizontal,QSlider::add-page:vertical,QSlider::handle {background: rgba(134, 206, 203, 1.0)}"
50 | "QComboBox::item:selected {background: rgba(134, 206, 203, 0.400);}"
51 | "QComboBox:focus, QComboBox:open {border-color: rgba(134, 206, 203, 1.0)}"
52 | "QComboBox::item:selected {border:none; background:rgba(134, 206, 203, 0.400); border-radius:4px}")
53 |
54 | if purpose == 'light':
55 | return ("QPushButton { color: rgba(19, 122, 127, 1.0); } "
56 | "QPushButton:hover { background:rgba(134, 206, 203, 0.110) } "
57 | "QPushButton:default { background: rgba(19, 122, 127, 1.0); } "
58 | "QPushButton:default:hover {background: rgba(19, 122, 127, 1.0); } "
59 | "QPushButton:default:pressed,QPushButton:default:checked {background: rgba(19, 122, 127, 1.0); } "
60 | "QLabel { selection-background-color: rgba(19, 122, 127, 0.65); } "
61 | "QTextEdit:focus, QTextEdit:selected, QLineEdit:focus, QLineEdit:selected { selection-background-color: rgba(19, 122, 127, 0.65); } "
62 | "QTextEdit:focus, QLineEdit:focus { border-color: rgba(19, 122, 127, 1.0); } "
63 | "QSplitter:handle:hover { background-color: rgba(19, 122, 127, 0.5); } "
64 | "QTabBar:tab:selected:enabled { color: rgba(19, 122, 127, 1.0); border-color: rgba(19, 122, 127, 1.0); } "
65 | "QProgressBar::chunk {background: rgba(19, 122, 127, 0.8); } "
66 | "QCheckBox:hover,QRadioButton:hover {border-bottom:2px solid rgba(19, 122, 127, 1.0); }"
67 | "QSlider::sub-page:horizontal,QSlider::add-page:vertical,QSlider::handle {background: rgba(19, 122, 127, 1.0)}"
68 | "QComboBox::item:selected {background: rgba(19, 122, 127, 0.400);}"
69 | "QComboBox:focus, QComboBox:open {border-color: rgba(19, 122, 127, 1.0)}"
70 | "QComboBox::item:selected {border:none; background:rgba(19, 122, 127, 0.400); border-radius:4px}")
71 |
72 |
73 | def custom_color(purpose: str):
74 | if purpose == 'Q_moved':
75 | return QColor(0, 112, 255, 255)
76 | elif purpose == 'Q_deleted':
77 | return QColor(204, 0, 34, 255)
78 | elif purpose == 'Q_matched':
79 | return QColor(19, 122, 127, 128)
80 | elif purpose == 'current':
81 | return 'rgba(134, 206, 203, 1.0)'
82 | elif purpose == 'moved':
83 | return 'rgba(0, 112, 255, 1.0)'
84 | elif purpose == 'deleted':
85 | return 'rgba(204, 0, 34, 1.0)'
86 | elif purpose == 'matched':
87 | return 'rgba(19, 122, 127, 1.0)'
88 | elif purpose == 'leave':
89 | return 'transparent'
90 | elif purpose == 'hover':
91 | return 'rgba(134, 206, 203, 1.0)'
92 | elif purpose == 'default':
93 | return 'palette(dark)'
94 | elif purpose == 'available' or purpose == 'txt2img':
95 | return 'palette(highlight)'
96 | elif purpose == 'PNG':
97 | return 'green'
98 | elif purpose == 'JPEG':
99 | return 'blue'
100 | elif purpose == 'WEBP' or purpose == 'img2img' or purpose == 'inpaint':
101 | return 'red'
102 | elif purpose == 'confidence':
103 | return 'QSlider::handle:horizontal {height: 0px; width: 0px; border-radius: 0px; }' \
104 | 'QSlider::sub-page {background: rgba(134, 206, 203, 0.8)}'
105 | else:
106 | return
107 |
108 |
109 | def custom_keybindings(parent):
110 |
111 | list_shortcut = QShortcut(QKeySequence('Ctrl+L'), parent)
112 | thumbnail_shortcut = QShortcut(QKeySequence('Ctrl+T'), parent)
113 | search_shortcut = QShortcut(QKeySequence('Ctrl+F'), parent)
114 | toggle_theme_shortcut = QShortcut(QKeySequence('Ctrl+D'), parent)
115 | toggle_tab_bar_shortcut = QShortcut(QKeySequence('Ctrl+B'), parent)
116 | add_tab_shortcut = QShortcut(QKeySequence('Ctrl+O'), parent)
117 | replace_tab_shortcut = QShortcut(QKeySequence('Ctrl+N'), parent)
118 | select_all_shortcut = QShortcut(QKeySequence('Ctrl+A'), parent)
119 | quit_shortcut = QShortcut(QKeySequence('Ctrl+Q'), parent)
120 |
121 | select_all_shortcut.setObjectName('select all')
122 | list_shortcut.setObjectName('list')
123 | thumbnail_shortcut.setObjectName('thumbnail')
124 | search_shortcut.setObjectName('search')
125 | toggle_theme_shortcut.setObjectName('theme')
126 | toggle_tab_bar_shortcut.setObjectName('bar_toggle')
127 | add_tab_shortcut.setObjectName('append_file')
128 | replace_tab_shortcut.setObjectName('replace_file')
129 | quit_shortcut.setObjectName('exit')
130 |
131 | if hasattr(parent, 'tab_signal_received'):
132 | list_shortcut.activated.connect(parent.tab_signal_received)
133 | thumbnail_shortcut.activated.connect(parent.tab_signal_received)
134 | search_shortcut.activated.connect(parent.tab_signal_received)
135 | toggle_tab_bar_shortcut.activated.connect(parent.tab_signal_received)
136 | toggle_theme_shortcut.activated.connect(parent.tab_signal_received)
137 | add_tab_shortcut.activated.connect(parent.tab_signal_received)
138 | replace_tab_shortcut.activated.connect(parent.tab_signal_received)
139 | quit_shortcut.activated.connect(parent.tab_signal_received)
140 |
141 | elif hasattr(parent, 'signal_received'):
142 | select_all_shortcut.activated.connect(parent.signal_received)
143 | list_shortcut.activated.connect(parent.signal_received)
144 | thumbnail_shortcut.activated.connect(parent.signal_received)
145 | search_shortcut.activated.connect(parent.signal_received)
146 | toggle_tab_bar_shortcut.activated.connect(parent.signal_received)
147 | toggle_theme_shortcut.activated.connect(parent.signal_received)
148 | add_tab_shortcut.activated.connect(parent.signal_received)
149 | replace_tab_shortcut.activated.connect(parent.signal_received)
150 | quit_shortcut.activated.connect(parent.signal_received)
151 |
152 |
153 | def custom_filename(filepath: str, category: str):
154 | if category == 'single':
155 | filename = config.get('JsonSingle', 'filename')
156 | if filename == 'filename':
157 | filename = os.path.splitext(os.path.basename(filepath))[0] + '.json'
158 |
159 | elif category == 'all':
160 | filename = config.get('JsonMultiple', 'directory')
161 | if filename == 'directory':
162 | filename = os.path.basename(os.path.dirname(filepath)) + '.json'
163 |
164 | else:
165 | filename = config.get('JsonSelected', 'selected')
166 | if filename == 'selected':
167 | filename = os.path.splitext(os.path.basename(filepath))[0] + '_and_so_on.json'
168 |
169 | return filename
170 |
171 |
172 | def custom_text(purpose):
173 | if purpose == '404':
174 | return "Couldn't find destination directory.\nPlease check your selected directory exists."
175 |
--------------------------------------------------------------------------------
/pyPromptChecker/gui/dialog.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | from PyQt6.QtWidgets import QFileDialog, QProgressDialog, QMessageBox, QLabel, QWidget, QVBoxLayout
5 | from PyQt6.QtWidgets import QDialog, QRadioButton, QPushButton, QHBoxLayout, QComboBox, QSlider, QGridLayout
6 | from PyQt6.QtCore import Qt, QTimer
7 | from .widget import move_centre
8 |
9 |
10 | class SelectDialog(QDialog):
11 | def __init__(self, parent=None):
12 | super().__init__(parent)
13 | self.setWindowTitle('Model hash extractor')
14 | self.selected = 0
15 | self.model = None
16 | self.lora = None
17 | self.__init_select_dialog()
18 | self.resize(200, 80)
19 |
20 | def __init_select_dialog(self):
21 | layout = QVBoxLayout()
22 | button_layout = QHBoxLayout()
23 |
24 | ok_button = QPushButton('OK')
25 | ok_button.clicked.connect(self.accept)
26 | cancel_button = QPushButton('Cancel')
27 | cancel_button.clicked.connect(self.reject)
28 | button_layout.addWidget(ok_button)
29 | button_layout.addWidget(cancel_button)
30 |
31 | self.model = QRadioButton('Model / VAE hash')
32 | self.model.setChecked(True)
33 | self.model.toggled.connect(self.__toggle_radio_button)
34 | self.lora = QRadioButton('LoRa / Textual inversion hash')
35 | self.lora.toggled.connect(self.__toggle_radio_button)
36 | layout.addWidget(self.model)
37 | layout.addWidget(self.lora)
38 | layout.addLayout(button_layout)
39 |
40 | self.setLayout(layout)
41 |
42 | def __toggle_radio_button(self):
43 | if self.model.isChecked():
44 | self.selected = 0
45 | elif self.lora.isChecked():
46 | self.selected = 1
47 |
48 |
49 | class InterrogateSelectDialog(QDialog):
50 | def __init__(self, parent=None):
51 | super().__init__(parent)
52 | self.setWindowTitle('Interrogate Settings')
53 | self.selected_model = 'moat'
54 | self.tag_threshold = 0.35
55 | self.tag_label = QLabel()
56 | self.chara_threshold = 0.85
57 | self.chara_label = QLabel()
58 |
59 | self.__init_interrogate_dialog()
60 |
61 | def __init_interrogate_dialog(self):
62 | root_layout = QGridLayout()
63 |
64 | for index, name in enumerate(('Model', 'Tag threshold', 'Character threshold')):
65 | label = QLabel(name + ' :')
66 | label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
67 | root_layout.addWidget(label, index, 0)
68 |
69 | if index == 0:
70 | value = QComboBox()
71 | value.addItems(('MOAT', 'Swin', 'ConvNext', 'ConvNextV2', 'ViT'))
72 | value.currentIndexChanged.connect(self.__model_change)
73 | root_layout.addWidget(value, index, 1, 1, 2)
74 |
75 | else:
76 | value = self.tag_threshold if index == 1 else self.chara_threshold
77 | int_value = self.tag_label if index == 1 else self.chara_label
78 | int_value.setText(str(value))
79 | int_value.setFixedSize(40, 25)
80 | root_layout.addWidget(int_value, index, 1)
81 |
82 | slider = QSlider()
83 | slider.setObjectName(name)
84 | slider.setRange(0, 100)
85 | slider.setMinimumWidth(200)
86 | slider.setValue(int(value * 100))
87 | slider.setOrientation(Qt.Orientation.Horizontal)
88 | slider.valueChanged.connect(self.__threshold_change)
89 | root_layout.addWidget(slider, index, 2)
90 |
91 | button_layout = QHBoxLayout()
92 | ok_button = QPushButton('OK')
93 | ok_button.clicked.connect(self.accept)
94 | cancel_button = QPushButton('Cancel')
95 | cancel_button.clicked.connect(self.reject)
96 | button_layout.addWidget(ok_button)
97 | button_layout.addWidget(cancel_button)
98 | root_layout.addLayout(button_layout, 3, 0, 1, 3)
99 |
100 | self.setLayout(root_layout)
101 |
102 | def __model_change(self):
103 | self.selected_model = self.sender().currentText()
104 |
105 | def __threshold_change(self):
106 | value = float(self.sender().value() / 100)
107 | if self.sender().objectName() == 'Tag threshold':
108 | self.tag_threshold = value
109 | self.tag_label.setText(str(value))
110 | else:
111 | self.chara_threshold = value
112 | self.chara_label.setText(str(value))
113 |
114 |
115 | class FileDialog(QFileDialog):
116 |
117 | def __init__(self, category: str, title: str, parent=None, file_filter: str = None, filename: str = None):
118 | super().__init__(parent)
119 | self.result = None
120 | self.file_filter = ''
121 | self.setWindowTitle(title)
122 | self.__set_filter(file_filter)
123 | self.setDirectory(os.path.expanduser('~'))
124 | self.category = category
125 | self.__set_category(self.category, filename)
126 |
127 | if self.exec():
128 | self.result = self.selectedFiles()
129 |
130 | def __set_category(self, category: str, filename: str):
131 | if category == 'save-file':
132 | self.setFileMode(QFileDialog.FileMode.AnyFile)
133 | self.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
134 | self.setNameFilter(self.file_filter)
135 | self.setOption(QFileDialog.Option.ShowDirsOnly, False)
136 | self.selectFile(filename)
137 | elif category == 'choose-files':
138 | self.setFileMode(QFileDialog.FileMode.ExistingFiles)
139 | self.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen)
140 | self.setNameFilter(self.file_filter)
141 | self.setOption(QFileDialog.Option.ShowDirsOnly, False)
142 | elif category == 'choose-directory':
143 | self.setFileMode(QFileDialog.FileMode.Directory)
144 | self.setOption(QFileDialog.Option.ShowDirsOnly, True)
145 |
146 | def __set_filter(self, str_filter: str):
147 | if str_filter == 'JSON':
148 | self.file_filter = 'JSON Files(*.json)'
149 | elif str_filter == 'PNG':
150 | self.file_filter = 'Image files(*.png *.jpg *.jpeg *.webp)'
151 |
152 |
153 | class ProgressDialog(QProgressDialog):
154 |
155 | def __init__(self, parent=None):
156 | super().__init__(parent)
157 | self.setWindowTitle("Progress")
158 | self.setWindowModality(Qt.WindowModality.WindowModal)
159 | self.setCancelButton(None)
160 | self.setMinimumDuration(0)
161 | self.setValue(0)
162 | self.now = 0
163 | move_centre(self)
164 |
165 | def update_value(self):
166 | now = self.now + 1
167 | self.setValue(now)
168 | self.now = now
169 |
170 |
171 | class MessageBox(QMessageBox):
172 | def __init__(self, text: str, title: str = 'pyPromptChecker', style: str = 'ok', icon: str = 'info', parent=None):
173 | super().__init__(parent)
174 | self.success = False
175 | self.setText(text)
176 | self.setWindowTitle(title)
177 | self.__set_style(style)
178 | self.__add_icon(icon)
179 |
180 | self.result = self.exec()
181 |
182 | if self.result == QMessageBox.StandardButton.Ok:
183 | self.success = True
184 |
185 | def __set_style(self, style: str):
186 | if 'ok' in style:
187 | self.addButton(QMessageBox.StandardButton.Ok)
188 | if 'no' in style:
189 | self.addButton(QMessageBox.StandardButton.No)
190 | if 'cancel' in style:
191 | self.addButton(QMessageBox.StandardButton.Cancel)
192 |
193 | def __add_icon(self, icon: str):
194 | if icon == 'critical':
195 | self.setIcon(QMessageBox.Icon.Critical)
196 | elif icon == 'warning':
197 | self.setIcon(QMessageBox.Icon.Warning)
198 | elif icon == 'question':
199 | self.setIcon(QMessageBox.Icon.Question)
200 | elif icon == 'no':
201 | self.setIcon(QMessageBox.Icon.NoIcon)
202 | else:
203 | self.setIcon(QMessageBox.Icon.Information)
204 |
205 |
206 | class Toast(QWidget):
207 | def __init__(self, parent=None):
208 | super().__init__(parent)
209 | self.timer = None
210 | self.message_label = QLabel()
211 | self.setWindowTitle("Toast")
212 | self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.WindowDoesNotAcceptFocus)
213 | self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
214 | self.setStyleSheet("background-color: rgba(50, 50, 50, 150); color: white; padding: 10px; border-radius: 5px;")
215 | self.hide()
216 |
217 | toast_layout = QVBoxLayout()
218 | toast_layout.addWidget(self.message_label)
219 | self.setLayout(toast_layout)
220 |
221 | def init_toast(self, message: str, duration: int = 2000):
222 | self.message_label.setText(message)
223 | self.show()
224 | self.adjustSize()
225 | adjust_x = int(self.sizeHint().width() / 2)
226 | adjust_y = int(self.sizeHint().height() / 2)
227 | width = self.parent().rect().width()
228 | height = self.parent().rect().height()
229 | x = int(width / 2)
230 | y = int(height / 2)
231 | self.move(x - adjust_x, y - adjust_y)
232 |
233 | self.timer = QTimer()
234 | self.timer.timeout.connect(self.__close_toast)
235 | self.timer.start(duration)
236 |
237 | def __close_toast(self):
238 | self.timer.stop()
239 | self.close()
240 |
--------------------------------------------------------------------------------
/pyPromptChecker/gui/listview.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from .dialog import *
4 | from .widget import *
5 | from .custom import *
6 | from . import config
7 |
8 | import os
9 | from PyQt6.QtCore import Qt, QTimer
10 | from PyQt6.QtWidgets import QMainWindow, QGridLayout, QVBoxLayout, QHBoxLayout, QScrollArea
11 | from PyQt6.QtWidgets import QWidget, QComboBox, QLabel
12 |
13 |
14 | LISTVIEW_PIXMAP = config.get('ListViewPixmapSize', 200)
15 | MOVE_DELETE = config.get('MoveDelete', False)
16 | HIDE_NOT_MATCH = config.get('HideNotMatchedTabs', False)
17 | BUTTONS = (('Select all', 'Select all'),
18 | ('Thumbnail', 'Thumbnail'),
19 | ('Tabview', 'Tabview'),
20 | ('Diff', 'Diff'),
21 | ('Interrogate', 'Interrogate'),
22 | ('Search', 'Search'),
23 | ('Add favourite', 'Add favourite'),
24 | ('▲M&enu', '▲Menu'))
25 |
26 |
27 | class Listview(QMainWindow):
28 | def __init__(self, parent=None, controller=None):
29 | super().__init__(parent)
30 | self.toast = None
31 | self.root_widget = None
32 | self.controller = controller
33 | self.size = LISTVIEW_PIXMAP
34 | self.setWindowTitle('Listview')
35 | custom_keybindings(self)
36 |
37 | self.header = None
38 | self.footer = None
39 | self.borders = []
40 |
41 | def init_listview(self, param_list: list, moved: set = None, deleted: set = None):
42 | progress = ProgressDialog()
43 | progress.setLabelText('Loading...')
44 | progress.setRange(0, len(param_list))
45 |
46 | self.borders = []
47 |
48 | estimated_height = 0
49 |
50 | self.root_widget = QWidget()
51 | root_layout = QVBoxLayout()
52 |
53 | central_widget = QWidget()
54 | central_widget_layout = QVBoxLayout()
55 |
56 | scroll_area = QScrollArea()
57 |
58 | for index, param in param_list:
59 | listview_border = ListviewBorder(index, param, self.controller, self.size, self)
60 | estimated_height += (listview_border.sizeHint().height() + 50)
61 | root_layout.addWidget(listview_border)
62 | self.borders.append(listview_border)
63 |
64 | if moved and index in moved:
65 | listview_border.set_moved()
66 |
67 | if deleted and index in deleted:
68 | listview_border.set_deleted()
69 |
70 | progress.update_value()
71 |
72 | self.root_widget.setLayout(root_layout)
73 |
74 | estimated_width = self.root_widget.sizeHint().width() + 50
75 | estimated_height = 800 if estimated_height > 800 else estimated_height
76 |
77 | scroll_area.setWidget(self.root_widget)
78 | scroll_area.setMinimumWidth(estimated_width)
79 | scroll_area.setMinimumHeight(estimated_height)
80 | scroll_area.setWidgetResizable(True)
81 | scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
82 |
83 | self.footer = FooterButtons(BUTTONS, self, self.controller)
84 |
85 | central_widget_layout.addLayout(self.__header_section())
86 | central_widget_layout.addWidget(scroll_area)
87 | central_widget_layout.addWidget(self.footer)
88 |
89 | central_widget.setLayout(central_widget_layout)
90 |
91 | self.setCentralWidget(central_widget)
92 |
93 | self.show()
94 | self.resize(estimated_width, estimated_height)
95 | move_centre(self)
96 |
97 | self.toast = Toast(self)
98 |
99 | def signal_received(self, right_click: bool = False):
100 | where_from = self.sender().objectName().lower()
101 | selected_index = set()
102 |
103 | for border in self.borders:
104 | if border.selected:
105 | selected_index.add(border.index)
106 | selected_index = tuple(selected_index)
107 |
108 | if where_from == 'add favourite':
109 | result = self.controller.request_reception('add', self, selected_index)
110 | if result:
111 | self.toast.init_toast('Added!', 1000)
112 | elif where_from == 'delete':
113 | result = self.controller.request_reception('delete', self, selected_index)
114 | if result:
115 | self.toast.init_toast('Added!', 1000)
116 | elif where_from == 'move':
117 | result = self.controller.request_reception('move', self, selected_index)
118 | if result:
119 | self.toast.init_toast('Moved!', 1000)
120 | elif where_from == 'interrogate':
121 | result = self.controller.request_reception('interrogate', self, selected_index)
122 | if result:
123 | self.toast.init_toast('Interrogated!', 1000)
124 | elif where_from == 'export jSON':
125 | result = self.controller.request_reception('json', self, selected_index)
126 | if result:
127 | self.toast.init_toast('Exported!', 1000)
128 | elif where_from == 'import json file replace':
129 | result = self.controller.request_reception('import', self, ('files', False))
130 | if result:
131 | self.toast.init_toast('Imported!', 1000)
132 | elif where_from == 'import json dir replace':
133 | result = self.controller.request_reception('import', self, ('files', False))
134 | if result:
135 | self.toast.init_toast('Imported!', 1000)
136 | elif where_from == 'append file':
137 | result = self.controller.request_reception('append', self, conditions='files')
138 | if result:
139 | self.toast.init_toast('Added!', 1000)
140 | elif where_from == 'append dir':
141 | result = self.controller.request_reception('append', self, conditions='directory')
142 | if result:
143 | self.toast.init_toast('Added!', 1000)
144 | elif where_from == 'replace_file':
145 | result = self.controller.request_reception('replace', self, conditions='files')
146 | if result:
147 | self.toast.init_toast('Replaced!', 1000)
148 | elif where_from == 'replace dir':
149 | result = self.controller.request_reception('replace', self, conditions='directory')
150 | if result:
151 | self.toast.init_toast('Replaced!', 1000)
152 | elif where_from == 'diff':
153 | self.controller.request_reception('diff', self, selected_index)
154 | elif where_from == 'search':
155 | self.controller.request_reception('search', self)
156 | elif where_from == 'thumbnail':
157 | self.controller.request_reception('thumbnail', self)
158 | elif where_from == 'tabview':
159 | self.controller.request_reception('tab', self)
160 | elif where_from == 'theme':
161 | self.controller.request_reception('theme', self)
162 | elif where_from == 'exit':
163 | self.controller.request_reception('exit', self)
164 | elif where_from == 'select all':
165 | self.__select_all_toggle(selected_index)
166 | elif where_from == 'restore':
167 | self.search_process()
168 | elif where_from == 'close':
169 | self.close()
170 |
171 | def listview_add_images(self, param_list: list):
172 | progress = None
173 | if self.isActiveWindow():
174 | progress = ProgressDialog()
175 | progress.setLabelText('Loading...')
176 | progress.setRange(0, len(param_list))
177 |
178 | layout = self.root_widget.layout()
179 |
180 | for index, param in param_list:
181 | listview_border = ListviewBorder(index, param, self.controller, self.size, self)
182 | layout.addWidget(listview_border)
183 | self.borders.append(listview_border)
184 |
185 | if progress:
186 | progress.update_value()
187 |
188 | def search_process(self, indexes: tuple = None):
189 | if indexes:
190 | for border in self.borders:
191 | if border.index in indexes:
192 | border.show()
193 | border.set_matched()
194 | else:
195 | border.clear_matched()
196 | if HIDE_NOT_MATCH:
197 | border.hide()
198 | else:
199 | for border in self.borders:
200 | border.show()
201 | border.clear_matched()
202 |
203 | def manage_subordinates(self, index: int, detail: str, remarks=None):
204 | for border in self.borders:
205 | if border.index == index:
206 | if detail == 'moved':
207 | border.set_moved()
208 | if remarks:
209 | border.title_change(remarks)
210 | if detail == 'deleted':
211 | border.set_deleted()
212 | if remarks:
213 | border.title_change(remarks)
214 | if border.selected:
215 | border.set_deselected()
216 | break
217 |
218 | def get_selected_images(self, selected: bool = True):
219 | result = []
220 | for border in self.borders:
221 | if border.selected and selected:
222 | result.append(border.index)
223 | elif not selected:
224 | result.append(border.index)
225 | return result
226 |
227 | def update_selected(self):
228 | indexes = self.get_selected_images(True)
229 | text = f'{str(len(indexes))} image selected'
230 | self.header.setText(text)
231 |
232 | def __header_section(self):
233 | row = 1
234 | col = 0
235 | header_layout = QGridLayout()
236 | self.header = QLabel('0 image selected')
237 | combo_items = ['Timestamp',
238 | 'Size',
239 | 'Seed',
240 | 'Sampler',
241 | 'Eta',
242 | 'Steps',
243 | 'CFG scale',
244 | 'Model',
245 | 'VAE',
246 | 'Var. seed',
247 | 'Var. strength',
248 | 'Resize from',
249 | 'Denoising',
250 | 'Clip skip',
251 | 'ENSD',
252 | 'Version'
253 | ]
254 |
255 | self.header.setMinimumWidth(50)
256 | self.header.setAlignment(Qt.AlignmentFlag.AlignCenter)
257 | header_layout.addWidget(self.header, 0, 0, 1, 4)
258 |
259 | for index, status in enumerate(('Timestamp', 'Seed', 'Sampler', 'Steps', 'CFG scale', 'Model', 'VAE', 'Version')):
260 | combo_box = QComboBox()
261 | combo_box.setObjectName(f'status_{index}')
262 | combo_box.currentIndexChanged.connect(self.__status_changed)
263 | header_layout.addWidget(combo_box, row, col)
264 | combo_box.addItems(combo_items)
265 | combo_box.setCurrentText(status)
266 | col += 1
267 | if col == 4:
268 | row += 1
269 | col = 0
270 |
271 | return header_layout
272 |
273 | def __status_changed(self):
274 | if self.centralWidget():
275 | status_number = self.sender().objectName().split('_')[1]
276 | status_str = self.sender().currentText()
277 |
278 | if status_str == 'Var. seed':
279 | search_str = 'Variation seed'
280 | elif status_str == 'Var. strength':
281 | search_str = 'Variation seed strength'
282 | elif status_str == 'Resize from':
283 | search_str = 'Seed resize from'
284 | elif status_str == 'Denoising':
285 | search_str = 'Denoising strength'
286 | else:
287 | search_str = status_str
288 |
289 | for border in self.borders:
290 | value = border.params.get(search_str, 'None')
291 | title_label = border.findChild(QLabel, status_number + '_title')
292 | value_label = border.findChild(QLabel, status_number + '_value')
293 |
294 | title_label.setText(status_str)
295 | value_label.setText(value)
296 |
297 | def __select_all_toggle(self, count: tuple):
298 | if len(count) == len(self.borders):
299 | for border in self.borders:
300 | border.set_deselected()
301 | else:
302 | for border in self.borders:
303 | border.set_selected()
304 |
305 |
306 | class ListviewBorder(ClickableGroup):
307 | def __init__(self, index: int, params: dict, controller, size: int = 200, parent=None):
308 | super().__init__(parent)
309 | self.controller = controller
310 | self.parent_window = parent
311 | self.size = size
312 | self.index = index
313 | self.params = params
314 | self.status_labels = []
315 | self.selected = False
316 | self.moved = False
317 | self.deleted = False
318 | self.matched = False
319 | self.changed = 0
320 | self.status = ['Timestamp', 'Seed', 'Sampler', 'Steps', 'CFG scale', 'Model', 'VAE', 'Version']
321 | self.__init_class()
322 |
323 | self.setObjectName(f'group_{index}')
324 | self.clicked.connect(self.__toggle_selected)
325 |
326 | def __init_class(self):
327 | layout = QHBoxLayout()
328 |
329 | layout.addWidget(self.__pixmap_label())
330 | layout.addLayout(self.__status_labels())
331 | layout.addLayout(self.__extension_labels())
332 |
333 | self.setLayout(layout)
334 | self.setTitle(self.params.get('Filepath', 'None'))
335 |
336 | def __pixmap_label(self):
337 | filepath = self.params.get('Filepath')
338 | pixmap = portrait_generator(filepath, self.size)
339 | pixmap_label = PixmapLabel()
340 | pixmap_label.setPixmap(pixmap)
341 | pixmap_label.setFixedSize(self.size, self.size)
342 | pixmap_label.clicked.connect(self.__toggle_selected)
343 | pixmap_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
344 | pixmap_label.setObjectName(f'pixmap_{self.index}')
345 | return pixmap_label
346 |
347 | def __status_labels(self):
348 | status_layout = QGridLayout()
349 | status_layout.setColumnMinimumWidth(0, 20)
350 | status_layout.setColumnMinimumWidth(0, 20)
351 |
352 | for index, key in enumerate(self.status):
353 | item = self.params.get(key, 'None')
354 |
355 | title_label = QLabel(key)
356 | title_label.setObjectName(f'{index}_title')
357 | status_label = QLabel(item)
358 | status_label.setObjectName(f'{index}_value')
359 |
360 | title_label.setFixedSize(100, 20)
361 | status_label.setFixedHeight(20)
362 | status_label.setMinimumWidth(200)
363 |
364 | status_layout.addWidget(title_label, index, 1)
365 | status_layout.addWidget(status_label, index, 3)
366 |
367 | return status_layout
368 |
369 | def __extension_labels(self):
370 | j = 0
371 | k = 0
372 | extension_layout = QGridLayout()
373 |
374 | creation = 'txt2img'
375 | if any(key in v for v in self.params for key in ('Upscaler', 'Extras')):
376 | creation = 'img2img'
377 | if self.params.get('Positive') == 'This file has no embedded data':
378 | creation = '---'
379 | if 'Mask blur' in self.params:
380 | creation = 'inpaint'
381 |
382 | addnet = any(key in v for v in self.params for key in ('Lora', 'Textual inversion', 'Add network'))
383 | cfg = any(key in v for v in self.params for key in ('Dynamic thresholding enabled', 'CFG auto', 'CFG scheduler'))
384 |
385 | for condition_list in [
386 | [self.params.get('Extensions', '---'), 'extension'],
387 | [creation, 'creation'],
388 | ['Extras', 'Extras' in self.params],
389 | ['Variation', 'Variation seed' in self.params],
390 | ['Hires.fix', 'Hires upscaler' in self.params],
391 | ['Lora/AddNet', addnet],
392 | ['CFG', cfg],
393 | ['Tiled Diffusion', 'Tiled diffusion' in self.params],
394 | ['ControlNet', 'ControlNet' in self.params],
395 | ['Regional', 'RP Active' in self.params]
396 | ]:
397 |
398 | title, status = condition_list
399 |
400 | extension_label = QLabel(title)
401 | extension_label.setFixedWidth(100)
402 | extension_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
403 |
404 | if not status or title == '---':
405 | extension_label.setDisabled(True)
406 | extension_label.setStyleSheet(custom_stylesheet('extension_label_disable', 'disabled'))
407 |
408 | elif status == 'extension' and title != '---':
409 | extension_label.setStyleSheet(custom_stylesheet('extension_label', title))
410 |
411 | elif status == 'creation' and title != '---':
412 | extension_label.setStyleSheet(custom_stylesheet('extension_label', title))
413 |
414 | else:
415 | extension_label.setStyleSheet(custom_stylesheet('extension_label', 'available'))
416 |
417 | extension_layout.addWidget(extension_label, j, k)
418 |
419 | if j == 4:
420 | k += 1
421 | j = 0
422 | else:
423 | j += 1
424 |
425 | return extension_layout
426 |
427 | def __toggle_selected(self):
428 | if self.parent_window.isActiveWindow():
429 | if 'group' in self.sender().objectName() and self.changed == 0:
430 | if self.selected:
431 | self.set_deselected()
432 | else:
433 | self.set_selected()
434 | self.changed = 1
435 |
436 | elif 'pixmap' in self.sender().objectName() and self.changed == 0:
437 | self.controller.request_reception('view', self.parent_window, (self.index,))
438 | self.changed = 1
439 |
440 | self.timer = QTimer()
441 | self.timer.timeout.connect(self.__initialize_changed)
442 | self.timer.start(10)
443 |
444 | def __initialize_changed(self):
445 | self.changed = 0
446 | self.timer.stop()
447 |
448 | def set_selected(self):
449 | current_stylesheet = self.styleSheet()
450 | current_stylesheet = custom_stylesheet('groupbox', 'current') + current_stylesheet
451 | self.setStyleSheet(current_stylesheet)
452 | self.selected = True
453 | self.parent_window.update_selected()
454 |
455 | def set_deselected(self):
456 | current_stylesheet = self.styleSheet()
457 | target = custom_stylesheet('groupbox', 'current')
458 | current_stylesheet = current_stylesheet.replace(target, '')
459 | self.setStyleSheet(current_stylesheet)
460 | self.selected = False
461 | self.parent_window.update_selected()
462 |
463 | def set_moved(self):
464 | self.moved = True
465 | self.deleted = False
466 | self.setStyleSheet(custom_stylesheet('title', 'moved'))
467 |
468 | def set_deleted(self):
469 | self.moved = False
470 | self.deleted = True
471 | self.setStyleSheet(custom_stylesheet('title', 'deleted'))
472 |
473 | def set_matched(self):
474 | self.matched = True
475 | self.setStyleSheet(custom_stylesheet('title', 'matched'))
476 |
477 | def clear_matched(self):
478 | if self.moved:
479 | self.set_moved()
480 | elif self.deleted:
481 | self.set_deleted()
482 | else:
483 | self.setStyleSheet('')
484 |
485 | def title_change(self, filepath: str):
486 | current_filepath = self.params.get('Filepath')
487 | if filepath != current_filepath:
488 | filename = os.path.basename(filepath)
489 | self.params['Filepath'] = filepath
490 | self.params['Filename'] = filename
491 | self.setTitle(filepath)
492 |
--------------------------------------------------------------------------------
/pyPromptChecker/gui/menu.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from PyQt6.QtWidgets import QMenu
4 | from PyQt6.QtGui import QAction
5 |
6 |
7 | class FileManageMenu(QMenu):
8 | def __init__(self, parent, controller):
9 | super().__init__(parent)
10 | self.main = parent
11 | self.controller = controller
12 |
13 | self.delete_menu = QMenu('Delete', self)
14 | self.delete = QAction('Confirm', self.delete_menu)
15 | self.delete.setObjectName('Delete')
16 | self.move_to = QAction('Move to', self)
17 | self.move_to.setObjectName('Move')
18 | self.add_favourite = QAction('Add favourite', self)
19 | self.add_favourite.setObjectName('Add favourite')
20 |
21 | self.__menu_position()
22 | self.__menu_trigger()
23 |
24 | def __menu_position(self):
25 | self.addMenu(self.delete_menu)
26 | self.delete_menu.addAction(self.delete)
27 |
28 | self.addSeparator()
29 |
30 | self.addAction(self.move_to)
31 | self.addAction(self.add_favourite)
32 |
33 | def __menu_trigger(self):
34 | self.delete.triggered.connect(lambda: self.__file_manage_menu_emit('add'))
35 | self.move_to.triggered.connect(lambda: self.__file_manage_menu_emit('move'))
36 | self.add_favourite.triggered.connect(lambda: self.__file_manage_menu_emit('delete'))
37 |
38 | def __file_manage_menu_emit(self, action):
39 | if hasattr(self.main, 'get_selected_images'):
40 | indexes = self.main.get_selected_images(True)
41 | self.controller.request_reception(action, self.main, indexes)
42 |
43 |
44 | class SearchMenu(QMenu):
45 | def __init__(self, parent, controller):
46 | super().__init__(parent)
47 | self.main = parent
48 | self.controller = controller
49 |
50 | self.restore = QAction('Restore', self)
51 | self.search = QAction('Search', self)
52 | self.init_search = QAction('Search initial image', self)
53 |
54 | self.__menu_position()
55 | self.__menu_trigger()
56 |
57 | def __menu_position(self):
58 | self.addAction(self.restore)
59 |
60 | self.addSeparator()
61 |
62 | self.addAction(self.search)
63 | self.addAction(self.init_search)
64 |
65 | def __menu_trigger(self):
66 | self.search.triggered.connect(lambda: self.controller.request_reception('search', self.main))
67 | self.restore.triggered.connect(self.__restore_search)
68 | self.init_search.triggered.connect(self.__search_init_image)
69 |
70 | def __search_init_image(self):
71 | if hasattr(self.main, 'get_selected_images'):
72 | indexes = self.main.get_selected_images(True)
73 | if len(indexes) == 1:
74 | self.controller.request_reception('init', self.main, indexes)
75 |
76 | def __restore_search(self):
77 | if hasattr(self.main, 'search_process'):
78 | self.main.search_process()
79 |
80 |
81 | class TabMenu(QMenu):
82 | def __init__(self, parent=None):
83 | super().__init__(parent)
84 |
85 | self.main = parent
86 |
87 | self.tab_link = QAction('Compare extension', self)
88 | self.addAction(self.tab_link)
89 | self.tab_link.setCheckable(True)
90 |
91 | self.tab_link.triggered.connect(self.main.toggle_tab_link)
92 |
93 |
94 | class MainMenu(QMenu):
95 | def __init__(self, parent, controller):
96 | super().__init__(parent)
97 |
98 | self.main = controller
99 | self.window = parent
100 |
101 | self.lora_wizard = QAction('LoRa wizard', self)
102 | self.interrogate_menu = QMenu('Interrogate', self)
103 | self.interrogate_wd14 = QMenu('WD 1.4 Tagger', self)
104 | self.interrogate_all = QAction('All images', self)
105 | self.interrogate_selected = QAction('Selected images', self)
106 | self.interrogate_this = QAction('This image', self)
107 |
108 | self.reselect_menu = QMenu('Reselect', self)
109 |
110 | self.reselect_from_file = QMenu('Select files', self.reselect_menu)
111 | self.reselect_add_file = QAction('Append images', self.reselect_from_file)
112 | self.reselect_renewal_file = QAction('Replace all images', self.reselect_from_file)
113 |
114 | self.reselect_from_dir = QMenu('Select directory', self.reselect_menu)
115 | self.reselect_add_dir = QAction('Append tabs', self.reselect_from_dir)
116 | self.reselect_renewal_dir = QAction('Replace all images', self.reselect_from_dir)
117 |
118 | self.json_export_menu = QMenu('Export JSON', self)
119 | self.json_export_single = QAction("Present image", self.json_export_menu)
120 | self.json_export_all = QAction("All images", self.json_export_menu)
121 | self.json_export_selected = QAction('Selected images', self)
122 |
123 | self.json_import_menu = QMenu('Import JSON', self)
124 | self.json_import_files = QAction("Select files", self.json_import_menu)
125 | self.json_import_directory = QAction("Select directory", self.json_import_menu)
126 |
127 | self.model_hash_extractor = QAction('Model hash extractor', self)
128 | self.close = QAction('Close', self)
129 | self.quit = QAction('Quit', self)
130 | self.dark_mode = QAction('Dark mode', self)
131 | self.dark_mode.setCheckable(True)
132 |
133 | self.__menu_position()
134 | self.__menu_trigger()
135 |
136 | self.theme_check()
137 |
138 | def __menu_position(self):
139 |
140 | self.addAction(self.model_hash_extractor)
141 |
142 | self.addSeparator()
143 |
144 | self.addAction(self.lora_wizard)
145 | self.interrogate_wd14.addActions([self.interrogate_all, self.interrogate_selected, self.interrogate_this])
146 | self.interrogate_menu.addMenu(self.interrogate_wd14)
147 | self.addMenu(self.interrogate_menu)
148 | self.lora_wizard.setDisabled(True)
149 |
150 | self.addSeparator()
151 |
152 | self.reselect_from_dir.addActions([self.reselect_add_dir, self.reselect_renewal_dir])
153 | self.reselect_menu.addMenu(self.reselect_from_dir)
154 | self.reselect_from_file.addActions([self.reselect_add_file, self.reselect_renewal_file])
155 | self.reselect_menu.addMenu(self.reselect_from_file)
156 | self.addMenu(self.reselect_menu)
157 |
158 | self.addSeparator()
159 |
160 | self.json_import_menu.addAction(self.json_import_files)
161 | self.json_import_menu.addAction(self.json_import_directory)
162 | self.addMenu(self.json_import_menu)
163 |
164 | self.json_export_menu.addAction(self.json_export_single)
165 | self.json_export_menu.addAction(self.json_export_all)
166 | self.json_export_menu.addAction(self.json_export_selected)
167 | self.addMenu(self.json_export_menu)
168 |
169 | self.addSeparator()
170 |
171 | self.addAction(self.dark_mode)
172 |
173 | self.addSeparator()
174 |
175 | self.addAction(self.quit)
176 |
177 | def __menu_trigger(self):
178 | self.quit.triggered.connect(self.__exit_app)
179 | self.dark_mode.triggered.connect(lambda: self.main.request_reception('theme', sender=self.window))
180 | self.model_hash_extractor.triggered.connect(lambda: self.main.request_reception('hash', sender=self.window))
181 | self.reselect_add_file.triggered.connect(lambda: self.__reselect_files('files', 'append'))
182 | self.reselect_renewal_file.triggered.connect(lambda: self.__reselect_files('files', 'replace'))
183 | self.reselect_add_dir.triggered.connect(lambda: self.__reselect_files('directory', 'append'))
184 | self.reselect_renewal_dir.triggered.connect(lambda: self.__reselect_files('directory', 'replace'))
185 | self.json_import_files.triggered.connect(lambda: self.__json_import('files', True))
186 | self.json_import_directory.triggered.connect(lambda: self.__json_import('directory', True))
187 | self.json_export_single.triggered.connect(lambda: self.__json_export('single'))
188 | self.json_export_all.triggered.connect(lambda: self.__json_export('all'))
189 | self.json_export_selected.triggered.connect(lambda: self.__json_export('select'))
190 | self.interrogate_this.triggered.connect(self.__interrogate_request)
191 | self.interrogate_all.triggered.connect(lambda: self.__interrogate_request('all'))
192 | self.interrogate_selected.triggered.connect(lambda: self.__interrogate_request('selected'))
193 |
194 | def __exit_app(self):
195 | self.main.request_reception('exit', self.window)
196 |
197 | def __reselect_files(self, which: str, replace_or_append: str):
198 | self.main.request_reception(replace_or_append, self.window, conditions=which)
199 |
200 | def __json_import(self, which: str, is_replace: bool):
201 | self.main.request_reception('import', self.window, indexes=(which, is_replace))
202 |
203 | def __json_export(self, which: str):
204 | if hasattr(self.window, 'get_selected_images'):
205 | result = None
206 | if which == 'select':
207 | result = self.window.get_selected_images(True)
208 | elif which == 'all':
209 | result = self.window.get_selected_images(False)
210 | elif which == 'single':
211 | if hasattr(self.window, 'root_tab'):
212 | result = (self.window.root_tab.currentIndex(),)
213 | if result:
214 | self.main.request_reception('json', self.window, indexes=result)
215 |
216 | def __interrogate_request(self, which: str):
217 | if hasattr(self.window, 'interrogate_emit'):
218 | if which == 'select':
219 | self.window.interrogate_emit('selected')
220 | elif which == 'all':
221 | self.window.interrogate_emit('entire')
222 | else:
223 | self.window.interrogate_emit()
224 | elif hasattr(self.window, 'get_selected_images'):
225 | indexes = None
226 | if which == 'selected':
227 | indexes = self.window.get_selected_images(True)
228 | elif which == 'all':
229 | indexes = self.window.get_selected_images(False)
230 | if indexes:
231 | self.main.request_reception('interrogate', self.window, indexes=indexes)
232 |
233 | def present_check(self, destination):
234 | if not self.main.request_reception('check', self.window):
235 | self.quit.setText('Close')
236 |
237 | if not hasattr(destination, 'root_tab'):
238 | self.json_export_single.setDisabled(True)
239 | self.interrogate_this.setDisabled(True)
240 | else:
241 | self.json_export_single.setDisabled(False)
242 | self.interrogate_this.setDisabled(False)
243 | if not destination.tab_bar.tab_bar_availability:
244 | self.json_export_selected.setDisabled(True)
245 |
246 | def theme_check(self):
247 | if self.main.dark:
248 | self.dark_mode.setChecked(True)
249 | else:
250 | self.dark_mode.setChecked(False)
251 |
--------------------------------------------------------------------------------
/pyPromptChecker/gui/search.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import re
4 | from PyQt6.QtWidgets import QDialog, QGridLayout, QGroupBox, QCheckBox, QSlider
5 | from PyQt6.QtWidgets import QWidget, QPushButton, QLabel, QLineEdit, QComboBox
6 | from PyQt6.QtWidgets import QRadioButton
7 | from PyQt6.QtGui import QRegularExpressionValidator
8 | from PyQt6.QtCore import Qt, QRegularExpression
9 |
10 | from .dialog import MessageBox
11 |
12 |
13 | class SearchWindow(QDialog):
14 | def __init__(self, model_list: list, controller, parent=None):
15 | super().__init__(parent)
16 | self.setWindowTitle("Search")
17 | self.controller = controller
18 | self.conditions = {}
19 | self.result = 'Tabs'
20 | self.caller = None
21 | self.prompt = None
22 | self.status = None
23 | self.extension = None
24 | self.search_box = None
25 | self.search_model = None
26 | self.search_seed_box = None
27 | self.search_cfg_label = None
28 | self.search_button = None
29 | self.central_widget = None
30 | self.__init_search_window(model_list)
31 |
32 | def __init_search_window(self, model_list: list):
33 | layout = QGridLayout()
34 |
35 | result_label = QLabel('Result shows: ')
36 | result_box = QComboBox()
37 | result_box.addItems(['Tabs', 'Listview', 'Thumbnails'])
38 | result_box.currentIndexChanged.connect(self.__result_change)
39 |
40 | prompt_group = QGroupBox()
41 | prompt_group.setTitle('Search Keywords')
42 | prompt_group.setCheckable(True)
43 | prompt_group.setChecked(True)
44 | prompt_group_layout = QGridLayout()
45 |
46 | search_label = QLabel('Search words : ')
47 | self.search_box = QLineEdit(self)
48 |
49 | for i, tmp in enumerate(['Positive', 'Negative', 'Region control']):
50 | checkbox = QCheckBox(tmp)
51 | checkbox.setObjectName(tmp)
52 | prompt_group_layout.addWidget(checkbox, 2, i + 1)
53 | if tmp == 'Positive':
54 | checkbox.setChecked(True)
55 |
56 | checkbox = QCheckBox('Case insensitive')
57 | checkbox.setObjectName('Case insensitive')
58 | prompt_group_layout.addWidget(checkbox, 3, 2, 1, 2)
59 |
60 | checkbox = QCheckBox('Use regex')
61 | checkbox.setObjectName('Use regex')
62 | checkbox.setDisabled(True)
63 | prompt_group_layout.addWidget(checkbox, 3, 1)
64 |
65 | prompt_group_layout.addWidget(search_label, 1, 0)
66 | prompt_group_layout.addWidget(self.search_box, 1, 1, 1, 3)
67 | prompt_group.setLayout(prompt_group_layout)
68 | self.prompt = prompt_group
69 |
70 | status_group = QGroupBox()
71 | status_group.setTitle('Status')
72 | status_group.setCheckable(True)
73 | status_group.setChecked(False)
74 | status_group_layout = QGridLayout()
75 |
76 | search_model_label = QLabel('Model : ')
77 | self.search_model = QComboBox()
78 | self.search_model.addItems(model_list)
79 |
80 | status_group_layout.addWidget(search_model_label, 0, 0)
81 | status_group_layout.addWidget(self.search_model, 0, 1, 1, 3)
82 |
83 | search_seed_label = QLabel('Search seed : ')
84 | self.search_seed_box = QLineEdit(self)
85 | reg_ex = QRegularExpression('^[0-9]*')
86 | validator = QRegularExpressionValidator(reg_ex)
87 | self.search_seed_box.setValidator(validator)
88 |
89 | self.search_cfg_label = QLabel('CFG : 0')
90 | search_cfg = QSlider()
91 | search_cfg.setOrientation(Qt.Orientation.Horizontal)
92 | search_cfg.setTickInterval(1)
93 | search_cfg.setRange(0, 40)
94 | search_cfg.valueChanged.connect(self.__value_change)
95 |
96 | for i, tmp in enumerate(['Less than', 'Equal to', 'Greater than']):
97 | radio_button = QRadioButton(tmp)
98 | radio_button.setObjectName(tmp)
99 | status_group_layout.addWidget(radio_button, 3, i + 1)
100 | if tmp == 'Equal to':
101 | radio_button.setChecked(True)
102 |
103 | status_group_layout.addWidget(search_seed_label, 1, 0)
104 | status_group_layout.addWidget(self.search_seed_box, 1, 1, 1, 3)
105 | status_group_layout.addWidget(self.search_cfg_label, 2, 0)
106 | status_group_layout.addWidget(search_cfg, 2, 1, 1, 3)
107 |
108 | status_group.setLayout(status_group_layout)
109 | self.status = status_group
110 |
111 | extension_group = QGroupBox()
112 | extension_group.setTitle('Extensions')
113 | extension_group.setCheckable(True)
114 | extension_group.setChecked(False)
115 | extension_group_layout = QGridLayout()
116 |
117 | for i, tmp in enumerate(['LoRa / AddNet', 'Hires / Extras', 'CFG']):
118 | checkbox = QCheckBox(tmp)
119 | checkbox.setObjectName(tmp)
120 | extension_group_layout.addWidget(checkbox, 0, i)
121 |
122 | for i, tmp in enumerate(['Tiled diffusion', 'ControlNet', 'Regional prompter']):
123 | checkbox = QCheckBox(tmp)
124 | checkbox.setObjectName(tmp)
125 | extension_group_layout.addWidget(checkbox, 1, i)
126 |
127 | extension_group.setLayout(extension_group_layout)
128 | self.extension = extension_group
129 |
130 | search_button = QPushButton("Search", self)
131 | search_button.clicked.connect(self.__do_search)
132 | close_button = QPushButton('Close', self)
133 | close_button.clicked.connect(self.window_close)
134 |
135 | layout.addWidget(result_label, 0, 0)
136 | layout.addWidget(result_box, 0, 1, 1, 3)
137 | layout.addWidget(prompt_group, 1, 0, 2, 4)
138 | layout.addWidget(status_group, 4, 0, 2, 4)
139 | layout.addWidget(extension_group, 6, 0, 2, 4)
140 | layout.addWidget(search_button, 8, 0, 1, 2)
141 | layout.addWidget(close_button, 8, 2, 1, 2)
142 |
143 | central_widget = QWidget()
144 | central_widget.setLayout(layout)
145 | self.setLayout(layout)
146 |
147 | def __result_change(self):
148 | self.result = self.sender().currentText()
149 |
150 | def __value_change(self):
151 | self.search_cfg_label.setText('CFG : ' + str(self.sender().value() * 0.5))
152 |
153 | def __do_search(self):
154 | self.conditions['Result'] = self.result
155 | if self.prompt.isChecked():
156 | self.conditions['Search'] = self.search_box.text()
157 | for tmp in self.prompt.findChildren(QCheckBox):
158 | key = tmp.objectName()
159 | self.conditions[key] = tmp.isChecked()
160 | self.conditions['Prompt'] = self.prompt.isChecked()
161 |
162 | if self.status.isChecked():
163 | relation = 'Greater than'
164 | for tmp in self.status.findChildren(QRadioButton):
165 | if tmp.isChecked():
166 | relation = tmp.objectName()
167 | break
168 |
169 | keys = ['Model', 'Seed', 'CFG', 'Relation']
170 | seek = [self.search_model.currentText(),
171 | self.search_seed_box.text(),
172 | self.search_cfg_label.text().replace('CFG : ', ''),
173 | relation]
174 |
175 | for index, key in enumerate(keys):
176 | self.conditions[key] = seek[index]
177 | self.conditions['Status'] = self.status.isChecked()
178 |
179 | if self.extension.isChecked():
180 | for tmp in self.extension.findChildren(QCheckBox):
181 | self.conditions[tmp.objectName()] = tmp.isChecked()
182 | self.conditions['Extension'] = self.extension.isChecked()
183 |
184 | if self.__validation():
185 | params = self.controller.request_reception('dictionary', self.caller)
186 | matched = search_images(self.conditions, params)
187 | if len(matched) > 0:
188 | match_text = str(len(matched)) + ' image(s) found !'
189 | else:
190 | match_text = 'There is no match to show.'
191 | self.controller.request_reception('apply', self.caller, indexes=(matched, match_text))
192 |
193 | def __validation(self):
194 | words = self.conditions.get('Search', 'None')
195 | count = words.count('"')
196 |
197 | if count % 2 != 0:
198 | text = 'There are not an even number of double quotes.'
199 | MessageBox(text, 'Please check it out', 'ok', 'info', self)
200 | return False
201 |
202 | if ' | ' in words:
203 | text = 'There is space on either side of |.'
204 | MessageBox(text, 'Please check it out', 'ok', 'info', self)
205 | return False
206 |
207 | return True
208 |
209 | def show_dialog(self, caller):
210 | self.caller = caller
211 | self.search_box.setFocus()
212 | self.show()
213 |
214 | def update_model_list(self, model_list: list):
215 | self.search_model.clear()
216 | self.search_model.addItems(model_list)
217 |
218 | def window_close(self):
219 | self.caller = None
220 | self.close()
221 |
222 |
223 | def cfg_checks(cfg_keywords: str, relation: str, targets: list):
224 | result = []
225 | for target in targets:
226 | if relation == 'Less than' and float(cfg_keywords) > float(target):
227 | result.append(True)
228 | elif relation == 'Equal to' and float(cfg_keywords) == float(target):
229 | result.append(True)
230 | elif relation == 'Greater than' and float(cfg_keywords) < float(target):
231 | result.append(True)
232 | else:
233 | result.append(False)
234 | return result
235 |
236 |
237 | def status_checks(keyword: str, targets: list):
238 | result = []
239 | for target in targets:
240 | if keyword == target:
241 | result.append(True)
242 | else:
243 | result.append(False)
244 | return result
245 |
246 |
247 | def parse_search_query(input_string: str):
248 | phrases = re.findall(r'"([^"]*)"', input_string)
249 | tmp_string = re.sub(r'"[^"]*"', 'REPLACEMENT_STRING', input_string)
250 | and_parts = tmp_string.split()
251 | or_parts = [value.split('|') for value in and_parts]
252 |
253 | for i, part in enumerate(or_parts):
254 | d2 = len(or_parts[i])
255 | for j in range(d2):
256 | if 'REPLACEMENT_STRING' in or_parts[i][j]:
257 | or_parts[i][j] = or_parts[i][j].replace('REPLACEMENT_STRING', f'{phrases.pop(0)}').strip()
258 | else:
259 | or_parts[i][j] = or_parts[i][j].strip()
260 |
261 | result = or_parts
262 |
263 | return result
264 |
265 |
266 | def target_string_adjust(positive: bool, negative: bool, region: bool, target: list):
267 | positive_target = []
268 | negative_target = []
269 | region_target = []
270 | result = []
271 |
272 | if positive:
273 | positive_target = [value.get('Positive') for value in target]
274 | if negative:
275 | negative_target = [value.get('Negative') for value in target]
276 | if region:
277 | for tg in target:
278 | number = tg.get('Region control', None)
279 | if not number:
280 | region_target.append('---')
281 | else:
282 | str_target = ''
283 | for cnt in range(1, int(number) + 1):
284 | prompt = tg.get('Region ' + str(cnt) + ' prompt')
285 | negative = tg.get('Region ' + str(cnt) + ' neg prompt')
286 | region_prompt = prompt + ', ' + negative
287 | str_target += region_prompt
288 | region_target.append(str_target)
289 |
290 | with_values = [x for x in [positive_target, negative_target, region_target] if x]
291 | values_count = len(with_values)
292 |
293 | if values_count == 1:
294 | result = [value for d1 in with_values for value in d1]
295 | elif values_count == 2:
296 | result = [str(x) + '\n' + str(y) for x, y in zip(*with_values)]
297 | elif values_count == 3:
298 | result = [str(x) + '\n' + str(y) + '\n' + str(z) for x, y, z in zip(*with_values)]
299 |
300 | return result
301 |
302 |
303 | def search_prompt_string(query: list, target_text: str, case: bool):
304 | if len(query) > 1 and isinstance(query, list):
305 | return any(search_prompt_string(query_value, target_text, case) for query_value in query)
306 | else:
307 | if isinstance(query, list):
308 | query = query[0]
309 | if case:
310 | return query.lower() in target_text.lower()
311 | else:
312 | return query in target_text
313 |
314 |
315 | def search_images(condition_list: dict, target_list: list):
316 | result = []
317 | prompt_result = []
318 | status_result = []
319 | extensions_result = []
320 | search_strings = condition_list.get('Search')
321 |
322 | if condition_list.get('Prompt') and search_strings:
323 | search_strings = parse_search_query(search_strings)
324 | case_insensitive = condition_list.get('Case insensitive')
325 | positive = condition_list.get('Positive')
326 | negative = condition_list.get('Negative')
327 | region = condition_list.get('Region control')
328 |
329 | target_data = target_string_adjust(positive, negative, region, target_list)
330 | for target in target_data:
331 | if not target or target == 'This file has no embedded data':
332 | target = '----'
333 | if all(search_prompt_string(query, target, case_insensitive) for query in search_strings):
334 | prompt_result.append(True)
335 | else:
336 | prompt_result.append(False)
337 |
338 | if condition_list.get('Status'):
339 | model_result = []
340 | seed_result = []
341 | cfg_result = []
342 | model_keyword = condition_list.get('Model')
343 | seed_keyword = condition_list.get('Seed')
344 | cfg_keyword = condition_list.get('CFG', '0')
345 | cfg_relation = condition_list.get('Relation')
346 |
347 | if model_keyword or seed_keyword or cfg_keyword:
348 | if model_keyword:
349 | model_target = [value.get('Model') for value in target_list]
350 | model_result = status_checks(model_keyword, model_target)
351 | if seed_keyword:
352 | seed_target = [value.get('Seed') for value in target_list]
353 | seed_result = status_checks(seed_keyword, seed_target)
354 | if float(cfg_keyword) > 0:
355 | cfg_target = [value.get('CFG scale') for value in target_list]
356 | cfg_result = cfg_checks(cfg_keyword, cfg_relation, cfg_target)
357 |
358 | result_with_values = [x for x in [model_result, seed_result, cfg_result] if x]
359 | result_counts = len(result_with_values)
360 |
361 | if result_counts > 1:
362 | tmp_status = list(zip(*result_with_values))
363 | for status in tmp_status:
364 | if all(status):
365 | status_result.append(True)
366 | else:
367 | status_result.append(False)
368 | else:
369 | tmp_status = [value for d1 in result_with_values for value in d1]
370 | for status in tmp_status:
371 | if status:
372 | status_result.append(True)
373 | else:
374 | status_result.append(False)
375 |
376 | if condition_list.get('Extension'):
377 | hires_keys = ['Hires upscaler', 'Face restoration', 'Extras']
378 | cfg_keys = ['Dynamic thresholding enabled', 'CFG auto', 'CFG scheduler']
379 | lora_keys = ['Lora', 'AddNet Enabled', 'Textual inversion']
380 | hires_enable = condition_list.get('Hires / Extras')
381 | lora_enable = condition_list.get('LoRa / AddNet')
382 | cfg_enable = condition_list.get('CFG')
383 | tiled_diffusion_enable = condition_list.get('Tiled diffusion')
384 | controlnet_enable = condition_list.get('ControlNet')
385 | regional_prompter_enable = condition_list.get('Regional prompter')
386 |
387 | lora = []
388 | hires = []
389 | cfg = []
390 | tiled = []
391 | control = []
392 | rp = []
393 |
394 | for target in target_list:
395 | if lora_enable:
396 | lora.append(any(key in v for v in target for key in lora_keys))
397 | if hires_enable:
398 | hires.append(any(key in v for v in target for key in hires_keys))
399 | if cfg_enable:
400 | cfg.append(any(key in v for v in target for key in cfg_keys))
401 | if tiled_diffusion_enable:
402 | tiled.append('Tiled diffusion' in target)
403 | if controlnet_enable:
404 | control.append('ControlNet' in target)
405 | if regional_prompter_enable:
406 | rp.append('RP Active' in target)
407 |
408 | result_with_values = [x for x in [lora, hires, cfg, tiled, control, rp] if x]
409 | result_counts = len(result_with_values)
410 |
411 | if result_counts > 1:
412 | tmp_status = list(zip(*result_with_values))
413 | for status in tmp_status:
414 | if all(status):
415 | extensions_result.append(True)
416 | else:
417 | extensions_result.append(False)
418 | else:
419 | tmp_status = [value for d1 in result_with_values for value in d1]
420 | for status in tmp_status:
421 | if status:
422 | extensions_result.append(True)
423 | else:
424 | extensions_result.append(False)
425 |
426 | no_empty_arrays = [x for x in [prompt_result, status_result, extensions_result] if x]
427 | array_counts = len(no_empty_arrays)
428 |
429 | if array_counts > 1:
430 | tmp_result = list(zip(*no_empty_arrays))
431 | for index, status in enumerate(tmp_result):
432 | if all(status):
433 | result.append(index)
434 | else:
435 | tmp_result = [value for d1 in no_empty_arrays for value in d1]
436 | for index, status in enumerate(tmp_result):
437 | if status:
438 | result.append(index)
439 |
440 | return result
441 |
--------------------------------------------------------------------------------
/pyPromptChecker/gui/thumbnail.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from .dialog import *
4 | from .widget import *
5 | from .custom import *
6 | from . import config
7 |
8 | import os
9 | from PyQt6.QtCore import Qt
10 | from PyQt6.QtWidgets import QMainWindow, QGridLayout, QVBoxLayout, QScrollArea
11 | from PyQt6.QtWidgets import QWidget
12 |
13 | THUMBNAIL_PIXMAP = config.get('ThumbnailPixmapSize', 150)
14 | MOVE_DELETE = config.get('MoveDelete', False)
15 | HIDE_NOT_MATCH = config.get('HideNotMatchedTabs', False)
16 | BUTTONS = (('Select all', 'Select all'),
17 | ('Listview', 'Listview'),
18 | ('Tabview', 'Tabview'),
19 | ('Diff', 'Diff'),
20 | ('Interrogate', 'Interrogate'),
21 | ('Search', 'Search'),
22 | ('Add favourite', 'Add favourite'),
23 | ('▲M&enu', '▲Menu'))
24 |
25 |
26 | class ThumbnailView(QMainWindow):
27 | def __init__(self, parent=None, controller=None):
28 | super().__init__(parent)
29 | self.toast = None
30 | self.controller = controller
31 | self.estimated_height = THUMBNAIL_PIXMAP + 67
32 | self.estimated_width = THUMBNAIL_PIXMAP + 40
33 | self.setWindowTitle('Thumbnail View')
34 | custom_keybindings(self)
35 |
36 | self.header = None
37 | self.footer = None
38 | self.borders = []
39 | self.pos_x = 0
40 | self.pos_y = 0
41 | self.max_x = 0
42 | self.max_y = 0
43 |
44 | def init_thumbnail(self, param_list: list, moved: set = None, deleted: set = None):
45 | progress = ProgressDialog()
46 | progress.setLabelText('Loading...')
47 | progress.setRange(0, len(param_list))
48 |
49 | thumbnails = QWidget()
50 | thumbnails.setObjectName('thumbnail_view')
51 | thumbnails_layout = QGridLayout()
52 |
53 | self.borders = []
54 | self.pos_x = 0
55 | self.pos_y = 0
56 | self.max_x = 0
57 | self.max_y = 0
58 |
59 | for index, param in param_list:
60 | self.max_y = self.pos_y + 1
61 | self.max_x = max(self.pos_x + 1, self.max_x)
62 | portrait_border = ThumbnailBorder(index, param, THUMBNAIL_PIXMAP, self.controller, self)
63 | self.borders.append(portrait_border)
64 | thumbnails_layout.addWidget(portrait_border, self.pos_y, self.pos_x)
65 |
66 | if moved and index in moved:
67 | portrait_border.set_moved()
68 |
69 | if deleted and index in deleted:
70 | portrait_border.set_deleted()
71 |
72 | if self.pos_x * self.estimated_width < 900:
73 | self.pos_x += 1
74 | else:
75 | self.pos_x = 0
76 | self.pos_y += 1
77 |
78 | progress.update_value()
79 |
80 | thumbnails.setLayout(thumbnails_layout)
81 |
82 | scroll_area = QScrollArea()
83 | scroll_area.setWidget(thumbnails)
84 |
85 | central_widget = QWidget()
86 | central_widget_layout = QVBoxLayout()
87 |
88 | self.header = QLabel('0 image selected')
89 | self.header.setAlignment(Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter)
90 | central_widget_layout.addWidget(self.header)
91 |
92 | scroll_area.setMinimumWidth(thumbnails.sizeHint().width() + 25)
93 | scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
94 | scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
95 | scroll_area.setWidgetResizable(True)
96 | central_widget_layout.addWidget(scroll_area)
97 |
98 | self.footer = FooterButtons(BUTTONS, self, self.controller)
99 | central_widget_layout.addWidget(self.footer)
100 | central_widget.setLayout(central_widget_layout)
101 |
102 | if self.centralWidget():
103 | self.centralWidget().deleteLater()
104 | self.setCentralWidget(central_widget)
105 |
106 | estimated_height = min(self.estimated_height * self.max_y + 80, 1000)
107 | estimated_width = self.estimated_width * self.max_x + 210
108 |
109 | self.show()
110 | self.resize(estimated_width, estimated_height + 25)
111 | move_centre(self)
112 |
113 | self.toast = Toast(self)
114 |
115 | progress.close()
116 |
117 | scroll_area.setMinimumWidth(0)
118 |
119 | def signal_received(self, right: bool = False):
120 | where_from = self.sender().objectName().lower()
121 | selected_index = set()
122 |
123 | for border in self.borders:
124 | if border.selected:
125 | selected_index.add(border.index)
126 | selected_index = tuple(selected_index)
127 |
128 | if where_from == 'add favourite':
129 | result = self.controller.request_reception('add', self, selected_index)
130 | if result:
131 | self.toast.init_toast('Added!', 1000)
132 | elif where_from == 'delete':
133 | result = self.controller.request_reception('delete', self, selected_index)
134 | if result:
135 | self.toast.init_toast('Added!', 1000)
136 | elif where_from == 'move':
137 | result = self.controller.request_reception('move', self, selected_index)
138 | if result:
139 | self.toast.init_toast('Moved!', 1000)
140 | elif where_from == 'interrogate':
141 | result = self.controller.request_reception('interrogate', self, selected_index)
142 | if result:
143 | self.toast.init_toast('Interrogated!', 1000)
144 | elif where_from == 'export jSON':
145 | result = self.controller.request_reception('json', self, selected_index)
146 | if result:
147 | self.toast.init_toast('Exported!', 1000)
148 | elif where_from == 'import json file replace':
149 | result = self.controller.request_reception('import', self, ('files', False))
150 | if result:
151 | self.toast.init_toast('Imported!', 1000)
152 | elif where_from == 'import json dir replace':
153 | result = self.controller.request_reception('import', self, ('files', False))
154 | if result:
155 | self.toast.init_toast('Imported!', 1000)
156 | elif where_from == 'append file':
157 | result = self.controller.request_reception('append', self, conditions='files')
158 | if result:
159 | self.toast.init_toast('Added!', 1000)
160 | elif where_from == 'append dir':
161 | result = self.controller.request_reception('append', self, conditions='directory')
162 | if result:
163 | self.toast.init_toast('Added!', 1000)
164 | elif where_from == 'replace_file':
165 | result = self.controller.request_reception('replace', self, conditions='files')
166 | if result:
167 | self.toast.init_toast('Replaced!', 1000)
168 | elif where_from == 'replace dir':
169 | result = self.controller.request_reception('replace', self, conditions='directory')
170 | if result:
171 | self.toast.init_toast('Replaced!', 1000)
172 | elif where_from == 'diff':
173 | self.controller.request_reception('diff', self, selected_index)
174 | elif where_from == 'search':
175 | self.controller.request_reception('search', self)
176 | elif where_from == 'listview':
177 | self.controller.request_reception('list', self)
178 | elif where_from == 'tabview':
179 | self.controller.request_reception('tab', self)
180 | elif where_from == 'theme':
181 | self.controller.request_reception('theme', self)
182 | elif where_from == 'exit':
183 | self.controller.request_reception('exit', self)
184 | elif where_from == 'select all':
185 | self.__select_all_toggle(selected_index)
186 | elif where_from == 'restore':
187 | self.search_process()
188 | elif where_from == 'close':
189 | self.close()
190 |
191 | def thumbnail_add_images(self, param_list: list):
192 | progress = None
193 |
194 | if self.isActiveWindow():
195 | progress = ProgressDialog()
196 | progress.setLabelText('Loading...')
197 | progress.setRange(0, len(param_list))
198 |
199 | layout = self.centralWidget().findChild(QWidget, 'thumbnail_view').layout()
200 |
201 | for index, param in param_list:
202 | self.max_y = self.pos_y + 1
203 | self.max_x = max(self.pos_x + 1, self.max_x)
204 | portrait_border = ThumbnailBorder(index, param, THUMBNAIL_PIXMAP, self.controller, self)
205 | self.borders.append(portrait_border)
206 | layout.addWidget(portrait_border, self.pos_y, self.pos_x)
207 |
208 | if self.pos_x * self.estimated_width < 900:
209 | self.pos_x += 1
210 | else:
211 | self.pos_x = 0
212 | self.pos_y += 1
213 |
214 | if progress:
215 | progress.update_value()
216 |
217 | def search_process(self, indexes: tuple = None):
218 | if indexes:
219 | for border in self.borders:
220 | if border.index in indexes:
221 | border.show()
222 | border.set_matched()
223 | else:
224 | border.clear_matched()
225 | if HIDE_NOT_MATCH:
226 | border.hide()
227 | else:
228 | for border in self.borders:
229 | border.show()
230 | border.clear_matched()
231 |
232 | def manage_subordinates(self, index: int, detail: str, remarks=None):
233 | for border in self.borders:
234 | if border.index == index:
235 | if detail == 'moved':
236 | border.set_moved()
237 | if remarks:
238 | border.label_change(remarks)
239 | if detail == 'deleted':
240 | border.set_deleted()
241 | if remarks:
242 | border.label_change(remarks)
243 | if border.selected:
244 | border.set_deselected()
245 | break
246 |
247 | def get_selected_images(self, selected=True):
248 | result = []
249 | for border in self.borders:
250 | if border.selected and selected:
251 | result.append(border.index)
252 | elif not selected:
253 | result.append(border.index)
254 | return result
255 |
256 | def update_selected(self):
257 | indexes = self.get_selected_images(True)
258 | text = f'{str(len(indexes))} image selected'
259 | self.header.setText(text)
260 |
261 | def __select_all_toggle(self, count: tuple):
262 | if len(count) == len(self.borders):
263 | for border in self.borders:
264 | border.set_deselected()
265 | else:
266 | for border in self.borders:
267 | border.set_selected()
268 |
269 |
270 | class ThumbnailBorder(ClickableGroup):
271 | def __init__(self, index: int, params: dict, size: int = 150, controller=None, parent=None):
272 | super().__init__(parent)
273 | self.parent_window = parent
274 | self.controller = controller
275 | self.size = size
276 | self.index = index
277 | self.params = params
278 | self.pixmap_label = PixmapLabel(self)
279 | self.label = QLabel(self)
280 | self.selected = False
281 | self.moved = False
282 | self.deleted = False
283 | self.matched = False
284 | self.__init_class()
285 |
286 | self.setObjectName(f'group_{self.index}')
287 | self.clicked.connect(self.__toggle_selected)
288 |
289 | def __init_class(self):
290 | layout = QVBoxLayout()
291 | self.setFixedSize(self.size + 60, self.size + 60)
292 |
293 | self.__pixmap_label()
294 | self.__filename_label()
295 |
296 | layout.addWidget(self.pixmap_label, alignment=Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter)
297 | layout.addWidget(self.label, alignment=Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignBottom)
298 | self.setLayout(layout)
299 |
300 | def __pixmap_label(self):
301 | filepath = self.params.get('Filepath')
302 | pixmap = portrait_generator(filepath, self.size)
303 | self.pixmap_label.setFixedSize(self.size, self.size)
304 | self.pixmap_label.setPixmap(pixmap)
305 | self.pixmap_label.setAlignment(Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter)
306 | self.pixmap_label.setObjectName(f'pixmap_{self.index}')
307 | self.pixmap_label.rightClicked.connect(self.__pixmap_clicked)
308 | self.pixmap_label.setToolTip(self.__tooltip())
309 |
310 | def __tooltip(self):
311 | result = ''
312 | for key in ('Filename', 'Timestamp', 'Seed', 'Sampler', 'Steps', 'CFG scale', 'Model', 'VAE', 'Version'):
313 | status = self.params.get(key, 'None')
314 | result += f'{key} : {status}\n'
315 | result = result.rstrip('\n')
316 | return result
317 |
318 | def __filename_label(self):
319 | filename = self.params.get('Filename')
320 | self.label.setStyleSheet(custom_stylesheet('label', 'leave'))
321 | self.label.setAlignment(Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter)
322 | self.label.setFixedHeight(30)
323 | self.label.setText(filename)
324 |
325 | def __pixmap_clicked(self):
326 | if self.parent_window.isActiveWindow():
327 | self.controller.request_reception('view', self.parent_window, indexes=(self.index,))
328 |
329 | def __toggle_selected(self):
330 | if self.parent_window.isActiveWindow():
331 | if self.selected:
332 | self.set_deselected()
333 | else:
334 | self.set_selected()
335 |
336 | def set_selected(self):
337 | self.selected = True
338 | self.setStyleSheet(custom_stylesheet('groupbox', 'current'))
339 | self.parent_window.update_selected()
340 |
341 | def set_deselected(self):
342 | self.selected = False
343 | self.setStyleSheet('')
344 | self.parent_window.update_selected()
345 |
346 | def set_moved(self):
347 | self.label.setStyleSheet(custom_stylesheet('colour', 'moved'))
348 | self.moved = True
349 | self.deleted = False
350 |
351 | def set_deleted(self):
352 | self.label.setStyleSheet(custom_stylesheet('colour', 'deleted'))
353 | self.moved = False
354 | self.deleted = True
355 |
356 | def set_matched(self):
357 | self.label.setStyleSheet(custom_stylesheet('colour', 'matched'))
358 | self.matched = True
359 |
360 | def clear_matched(self):
361 | if self.moved:
362 | self.set_moved()
363 | elif self.deleted:
364 | self.set_deleted()
365 | else:
366 | self.label.setStyleSheet(custom_stylesheet('label', 'leave'))
367 |
368 | def label_change(self, filepath: str):
369 | current_filepath = self.params.get('Filepath')
370 | if filepath != current_filepath:
371 | filename = os.path.basename(filepath)
372 | self.params['Filepath'] = filepath
373 | self.params['Filename'] = filename
374 | self.label.setText(filename)
375 | self.pixmap_label.setToolTip(self.__tooltip())
376 |
377 | def enterEvent(self, event):
378 | current_style = self.label.styleSheet()
379 |
380 | if current_style is not None:
381 | stylesheet = custom_stylesheet('label', 'hover')
382 | current_style += ';' + stylesheet
383 | self.label.setStyleSheet(current_style)
384 |
385 | def leaveEvent(self, event):
386 | current_style = self.label.styleSheet()
387 |
388 | if current_style is not None:
389 | stylesheet = custom_stylesheet('label', 'leave')
390 | target_part = custom_stylesheet('label', 'hover')
391 | current_style = current_style.replace(target_part, stylesheet)
392 | self.label.setStyleSheet(current_style)
393 |
--------------------------------------------------------------------------------
/pyPromptChecker/gui/viewer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import difflib
4 | from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QHBoxLayout, QGridLayout
5 | from PyQt6.QtWidgets import QScrollArea, QLabel, QTextEdit, QPushButton, QStackedWidget
6 | from PyQt6.QtGui import QPixmap
7 | from PyQt6.QtCore import Qt
8 |
9 | from .widget import PixmapLabel
10 | from .widget import move_centre
11 |
12 |
13 | class ImageWindow(QMainWindow):
14 | def __init__(self, parent=None):
15 | super().__init__(parent)
16 | self.screen = QApplication.primaryScreen()
17 | self.filepath = ''
18 | self.max_screen = self.screen.availableGeometry()
19 | self.screen_center = self.screen.geometry().center()
20 |
21 | def __image_window_clicked(self):
22 | if self.isActiveWindow():
23 | self.close()
24 |
25 | def init_image_window(self):
26 | label = PixmapLabel()
27 | pixmap = QPixmap(self.filepath)
28 |
29 | screen_width = int(self.max_screen.width() * 0.95)
30 | screen_height = int(self.max_screen.height() * 0.95)
31 | pixmap_width = pixmap.width()
32 | pixmap_height = pixmap.height()
33 |
34 | width = screen_width if pixmap_width > screen_width else pixmap_width
35 | height = screen_height if pixmap_height > screen_height else pixmap_height
36 |
37 | pixmap = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
38 |
39 | title = 'Image: ' + str(pixmap.width()) + 'x' + str(pixmap.height())
40 | self.setWindowTitle(title)
41 |
42 | label.setPixmap(pixmap)
43 | label.setAlignment(Qt.AlignmentFlag.AlignCenter)
44 | label.clicked.connect(self.__image_window_clicked)
45 |
46 | self.setCentralWidget(label)
47 | visible = self.isVisible()
48 | self.show()
49 | self.adjustSize()
50 |
51 | if not visible:
52 | frame_geometry = self.frameGeometry()
53 | frame_geometry.moveCenter(self.screen_center)
54 | self.move(frame_geometry.topLeft())
55 |
56 |
57 | class DiffWindow(QMainWindow):
58 | def __init__(self, params: tuple, parent=None):
59 | super().__init__(parent)
60 | self.status = None
61 | self.params = params
62 | self.setWindowTitle('Diff Window')
63 | self.__init_diff()
64 | self.setMinimumSize(1000, 1000)
65 | move_centre(self)
66 |
67 | def __init_diff(self):
68 | statuses = ['Extensions',
69 | 'Timestamp',
70 | 'Image size',
71 | 'Size',
72 | 'Seed',
73 | 'Sampler',
74 | 'Eta',
75 | 'Steps',
76 | 'CFG scale',
77 | 'Model',
78 | 'VAE',
79 | 'Variation seed',
80 | 'Variation seed strength',
81 | 'Seed resize from',
82 | 'Denoising strength',
83 | 'Clip skip',
84 | 'ENSD',
85 | 'Version'
86 | ]
87 | central_widget = QWidget()
88 | central_widget_layout = QHBoxLayout()
89 | diff = QWidget()
90 | diff_layout = QGridLayout()
91 |
92 | for i in range(len(self.params)):
93 | filepath = self.params[i].get('Filepath')
94 | filepath_label = QLabel(filepath)
95 | filepath_label.setFixedSize(500, 25)
96 | filepath_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
97 | diff_layout.addWidget(filepath_label, 0, i)
98 |
99 | pixmap_label = PixmapLabel()
100 | pixmap = QPixmap(filepath)
101 | pixmap = pixmap.scaled(500, 500, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
102 | pixmap_label.setPixmap(pixmap)
103 | pixmap_label.setAlignment(Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter)
104 | pixmap_label.setFixedSize(500, 500)
105 | diff_layout.addWidget(pixmap_label, 1, i)
106 |
107 | status_widget = QStackedWidget()
108 |
109 | page0 = QWidget()
110 | page0_layout = QHBoxLayout()
111 | scroll_source = QScrollArea()
112 | scroll_target = QScrollArea()
113 | scroll_area_source = QWidget()
114 | scroll_area_target = QWidget()
115 | scroll_layout_source = QGridLayout()
116 | scroll_layout_target = QGridLayout()
117 |
118 | html_tag = '@@@'
119 | for index, status in enumerate(statuses):
120 | source = self.params[0].get(status, 'None')
121 | target = self.params[1].get(status, 'None')
122 |
123 | if not source == target and index > 1:
124 | status = html_tag.replace('@@@', status)
125 | source = html_tag.replace('@@@', source)
126 | target = html_tag.replace('@@@', target)
127 |
128 | key_source = QLabel()
129 | key_source.setTextFormat(Qt.TextFormat.RichText)
130 | key_source.setText(status)
131 |
132 | key_target = QLabel()
133 | key_target.setTextFormat(Qt.TextFormat.RichText)
134 | key_target.setText(status)
135 |
136 | item_source = QLabel()
137 | item_source.setTextFormat(Qt.TextFormat.RichText)
138 | item_source.setText(source)
139 |
140 | item_target = QLabel()
141 | item_target.setTextFormat(Qt.TextFormat.RichText)
142 | item_target.setText(target)
143 |
144 | scroll_layout_source.addWidget(key_source, index, 0)
145 | scroll_layout_source.addWidget(item_source, index, 1)
146 | scroll_layout_target.addWidget(key_target, index, 0)
147 | scroll_layout_target.addWidget(item_target, index, 1)
148 |
149 | scroll_area_source.setLayout(scroll_layout_source)
150 | scroll_area_target.setLayout(scroll_layout_target)
151 | scroll_source.setWidget(scroll_area_source)
152 | scroll_target.setWidget(scroll_area_target)
153 |
154 | scroll_source.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
155 | scroll_source.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
156 | scroll_target.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
157 | scroll_target.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
158 |
159 | page0_layout.addWidget(scroll_source)
160 | page0_layout.addWidget(scroll_target)
161 | page0.setLayout(page0_layout)
162 |
163 | page1 = QWidget()
164 | page1_layout = QHBoxLayout()
165 | source_textbox, target_textbox = self.__make_textbox('Positive')
166 | page1_layout.addWidget(source_textbox)
167 | page1_layout.addWidget(target_textbox)
168 | page1.setLayout(page1_layout)
169 |
170 | page2 = QWidget()
171 | page2_layout = QHBoxLayout()
172 | source_textbox, target_textbox = self.__make_textbox('Negative')
173 | page2_layout.addWidget(source_textbox)
174 | page2_layout.addWidget(target_textbox)
175 | page2.setLayout(page2_layout)
176 |
177 | status_widget.addWidget(page0)
178 | status_widget.addWidget(page1)
179 | status_widget.addWidget(page2)
180 |
181 | diff_layout.addWidget(status_widget, 2, 0, 4, 2)
182 | self.status = status_widget
183 |
184 | button_layout = QHBoxLayout()
185 | close_button = QPushButton('Close')
186 | page0_button = QPushButton('Main status')
187 | page1_button = QPushButton('Positive')
188 | page2_button = QPushButton('Negative')
189 | close_button.clicked.connect(lambda: self.close())
190 | page0_button.clicked.connect(lambda: self.status.setCurrentIndex(0))
191 | page1_button.clicked.connect(lambda: self.status.setCurrentIndex(1))
192 | page2_button.clicked.connect(lambda: self.status.setCurrentIndex(2))
193 | button_layout.addWidget(page0_button)
194 | button_layout.addWidget(page1_button)
195 | button_layout.addWidget(page2_button)
196 | button_layout.addWidget(close_button)
197 | diff_layout.addLayout(button_layout, 6, 0, 1, 2)
198 |
199 | diff.setLayout(diff_layout)
200 | central_widget_layout.addWidget(diff)
201 | central_widget.setLayout(central_widget_layout)
202 | self.setCentralWidget(central_widget)
203 |
204 | self.show()
205 |
206 | def __make_textbox(self, key: str):
207 | original_source_words = self.params[0].get(key, 'None').replace('<', '<').replace('>', '>')
208 | original_target_words = self.params[1].get(key, 'None').replace('<', '<').replace('>', '>')
209 | original_source_words = original_source_words.replace('-', '').replace('@', '')
210 | original_target_words = original_target_words.replace('-', '').replace('@', '')
211 | source_words = original_source_words
212 | target_words = original_target_words
213 | common_words_source = []
214 | common_words_target = []
215 |
216 | while True:
217 | source_start, source_end, target_start, target_end = common_match(source_words, target_words)
218 |
219 | if source_start is None:
220 | break
221 |
222 | common_words_source.append((source_start, source_end))
223 | common_words_target.append((target_start, target_end))
224 |
225 | source_size = source_end - source_start
226 | target_size = target_end - target_start
227 |
228 | source_words = source_words[:source_start] + '-' * source_size + source_words[source_end:]
229 | target_words = target_words[:target_start] + '@' * target_size + target_words[target_end:]
230 |
231 | common_words_source = sorted(common_words_source, reverse=True)
232 | common_words_target = sorted(common_words_target, reverse=True)
233 |
234 | for starts, ends in common_words_source:
235 | original_source_words = insert_tag(original_source_words, starts, ends)
236 |
237 | for starts, ends in common_words_target:
238 | original_target_words = insert_tag(original_target_words, starts, ends)
239 |
240 | original_source_words = original_source_words.replace('\n', ' ').replace('', '-').replace('', '@')
241 | original_target_words = original_target_words.replace('\n', ' ').replace('', '-').replace('', '@')
242 |
243 | original_source_words = '' + original_source_words + ''
244 | original_target_words = '' + original_target_words + ''
245 |
246 | source_textbox = QTextEdit()
247 | source_textbox.setHtml(original_source_words)
248 | target_textbox = QTextEdit()
249 | target_textbox.setHtml(original_target_words)
250 |
251 | return source_textbox, target_textbox
252 |
253 |
254 | def common_match(diff1: str, diff2: str):
255 | matcher = difflib.SequenceMatcher(None, diff1, diff2, autojunk=False)
256 | match = matcher.find_longest_match(0, len(diff1), 0, len(diff2))
257 |
258 | if 0 < match.size < 5:
259 | if diff1[match.a - 1] not in '-@' and \
260 | diff1[match.a + match.size] not in '-@' and \
261 | diff2[match.b - 1] not in '-@' and \
262 | diff2[match.b + match.size] not in '-@':
263 | return None, None, None, None
264 |
265 | if match.size > 5:
266 | start1 = match.a
267 | start2 = match.b
268 | end1 = start1 + match.size
269 | end2 = start2 + match.size
270 | return start1, end1, start2, end2
271 |
272 | else:
273 | return None, None, None, None
274 |
275 |
276 | def insert_tag(target: str, start_pos: int, end_pos: int):
277 | tmp = target[:end_pos] + '' + target[end_pos:]
278 | result = tmp[:start_pos] + '' + tmp[start_pos:]
279 | return result
280 |
--------------------------------------------------------------------------------
/pyPromptChecker/icon/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yui-tan/pyPromptChecker/8a93d098df0888d81afcbf67e8a3ec6851cd9e5b/pyPromptChecker/icon/icon.ico
--------------------------------------------------------------------------------
/pyPromptChecker/icon/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yui-tan/pyPromptChecker/8a93d098df0888d81afcbf67e8a3ec6851cd9e5b/pyPromptChecker/icon/icon.png
--------------------------------------------------------------------------------
/pyPromptChecker/lib/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from .decoder import *
4 | from .io import *
5 | from .parser import ChunkData
6 |
--------------------------------------------------------------------------------
/pyPromptChecker/lib/decoder.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import json
4 | import png
5 | from PIL import Image
6 |
7 |
8 | def is_json_check(filepath):
9 | try:
10 | with open(filepath, 'r') as f:
11 | json.load(f)
12 | return True
13 | except (json.JSONDecodeError, FileNotFoundError, UnicodeDecodeError):
14 | return False
15 |
16 |
17 | def image_format_identifier(filepath):
18 | with open(filepath, 'rb') as f:
19 | png_signature = b'\x89PNG\r\n\x1a\n'
20 | jpeg_signature = b'\xff\xd8\xff'
21 | webp_head_signature = b'\x52\x49\x46\x46'
22 | webp_foot_signature = b'\x57\x45\x42\x50'
23 | file_header = f.read(12)
24 |
25 | if file_header.startswith(png_signature):
26 | return [filepath, 0]
27 | elif file_header.startswith(jpeg_signature):
28 | return [filepath, 1]
29 | elif file_header.startswith(webp_head_signature) and file_header.endswith(webp_foot_signature):
30 | return [filepath, 2]
31 | elif is_json_check(filepath):
32 | return [filepath, 9]
33 | else:
34 | return None
35 |
36 |
37 | def chunk_text_extractor(target, method, index=1):
38 | width = '--'
39 | height = '--'
40 |
41 | if method == 0:
42 | index = max(index, 1)
43 |
44 | try:
45 | reader = png.Reader(filename=target)
46 | chunks = reader.chunks()
47 | chunk_list = list(chunks)
48 |
49 | for chunk_type, chunk_data in chunk_list:
50 | if chunk_type == b'IHDR':
51 | width = int.from_bytes(chunk_data[0:4], byteorder='big')
52 | height = int.from_bytes(chunk_data[4:8], byteorder='big')
53 | break
54 |
55 | original_size = f'{width}x{height}'
56 |
57 | if index >= len(chunk_list):
58 | print('{} has no embedded data!'.format(target))
59 | return None, None
60 |
61 | text = ''
62 | ends = min(index + 4, len(chunk_list) - 1)
63 | for i in range(index, ends):
64 | chunk_data = chunk_list[i][1]
65 | str_data = chunk_data.decode('utf-8', errors='ignore').replace("\x00", "")
66 | if str_data.startswith('parameters'):
67 | text = text + str_data
68 | elif str_data.startswith('extras'):
69 | if text:
70 | text = text + str_data.replace('extras', ',')
71 | else:
72 | text = text + str_data.replace('extras', 'parameters')
73 |
74 | if text.startswith('parameters') and not text.startswith('parametersNone'):
75 | return text, original_size
76 | else:
77 | print('{} has not valid parameters'.format(target))
78 | return None, original_size
79 |
80 | except Exception as e:
81 | print('An error occurred while decoding: {}\n{}'.format(target, str(e)))
82 | return None, None
83 |
84 | elif method == 1 or method == 2:
85 | exif_id = 37510
86 |
87 | try:
88 | img = Image.open(target)
89 | exif = img._getexif()
90 | if exif:
91 | binary = exif.get(exif_id, b'')
92 | text = binary.decode('utf-8', errors='ignore').replace("\x00", "")
93 | else:
94 | text = 'no embedded data'
95 |
96 | width, height = img.size
97 |
98 | if text.startswith('UNICODE'):
99 | text = text.replace('UNICODE', 'parameters', 1)
100 | original_size = f'{width}x{height}'
101 | return text, original_size
102 | else:
103 | original_size = f'{width}x{height}'
104 | print('{} has not valid parameters'.format(target))
105 | return None, original_size
106 |
107 | except Exception as e:
108 | print('An error occurred while decoding: {}\n{}'.format(target, str(e)))
109 | return None, None
110 |
--------------------------------------------------------------------------------
/pyPromptChecker/lib/io.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from PIL import Image
4 |
5 | import uuid
6 | import csv
7 | import json
8 | import os
9 | import shutil
10 | import hashlib
11 |
12 |
13 | def export_file(data, kind, filepath):
14 | try:
15 | if kind == 'csv':
16 | with open(filepath, 'a') as f:
17 | writer = csv.writer(f, lineterminator='\n')
18 | writer.writerows(data)
19 | elif kind == 'text':
20 | with open(filepath, 'w') as f:
21 | f.write(data)
22 | elif kind == 'json':
23 | with open(filepath, 'w') as f:
24 | json.dump(data, f, sort_keys=True, indent=4, ensure_ascii=False)
25 | return True, None
26 | except Exception as e:
27 | return 'Error occurred during writing file.', e
28 |
29 |
30 | def io_export_json(json_data, filepath):
31 | try:
32 | with open(filepath, 'w') as f:
33 | json.dump(json_data, f, sort_keys=True, indent=4, ensure_ascii=False)
34 | return True, None
35 |
36 | except Exception as e:
37 | return 'Error occurred during writing JSON.', e
38 |
39 |
40 | def import_json(filepath):
41 | try:
42 | with open(filepath) as j:
43 | json_data = json.load(j)
44 | return json_data, None
45 |
46 | except json.JSONDecodeError as e:
47 | print('This is invalid JSON\n' + str(e) + '\n {}'.format(filepath))
48 | return None, e
49 |
50 | except Exception as e:
51 | return 'Error occurred during loading JSON.', e
52 |
53 |
54 | def import_model_list(filepath):
55 | if os.path.exists(filepath):
56 | with open(filepath, encoding='utf8', newline='') as f:
57 | csvreader = csv.reader(f)
58 | model_list = [row for row in csvreader]
59 | return model_list
60 | else:
61 | return None
62 |
63 |
64 | def image_copy_to(source, destination, use_copy=True):
65 | destination_path = os.path.join(destination, os.path.basename(source))
66 |
67 | if os.path.exists(destination_path):
68 | base, ext = os.path.splitext(destination_path)
69 | unique_suffix = str(uuid.uuid4())[:8]
70 | destination_path = os.path.join(destination, f"{base}-{unique_suffix}{ext}")
71 |
72 | if os.path.exists(source):
73 | try:
74 | shutil.copy(source, destination_path)
75 | if os.path.exists(destination_path) and not use_copy:
76 | os.remove(source)
77 | return destination_path, None
78 | return destination_path, None
79 |
80 | except Exception as e:
81 | return 'Error occurred moving/copying files.', e
82 | else:
83 | return "The source file doesn't exists.", 'FileNotFoundError'
84 |
85 |
86 | def clear_trash_bin(directory_path):
87 | file_list = os.listdir(directory_path)
88 |
89 | for filename in file_list:
90 | filepath = os.path.join(directory_path, filename)
91 | try:
92 | os.remove(filepath)
93 |
94 | except Exception as e:
95 | return False, e
96 |
97 | return True
98 |
99 |
100 | def is_directory_empty(directory_path):
101 | file_list = os.listdir(directory_path)
102 | return len(file_list) == 0
103 |
104 |
105 | def extract_lora_hash(filepath):
106 | lora_hash = hashlib.sha256()
107 | block = 1024 * 1024
108 |
109 | with open(filepath, 'rb') as f:
110 | f.seek(0)
111 | header = f.read(8)
112 | n = int.from_bytes(header, "little")
113 | offset = n + 8
114 | f.seek(offset)
115 |
116 | for chunk in iter(lambda: f.read(block), b''):
117 | lora_hash.update(chunk)
118 |
119 | return lora_hash.hexdigest()
120 |
121 |
122 | def extract_model_hash(filepath):
123 | model_hash = hashlib.sha256()
124 | block = 1024 * 1024
125 |
126 | with open(filepath, 'rb') as f:
127 | for chunk in iter(lambda: f.read(block), b''):
128 | model_hash.update(chunk)
129 |
130 | return model_hash.hexdigest()
131 |
132 |
133 | def extract_image_hash(filepath):
134 | img = Image.open(filepath)
135 | return hashlib.md5(img.tobytes()).hexdigest()
136 |
--------------------------------------------------------------------------------
/pyPromptChecker/lib/parser.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | import re
5 | import datetime
6 |
7 | PROMPT_REGEX = r'([\S\s]*)(?=Steps: )'
8 | LORA_HASH_REGEX = r'(?<=Lora hashes: )"[^"]*"'
9 | TI_HASH_REGEX = r'(?<=TI hashes: )"[^"]*"'
10 | TILED_DIFFUSION_REGEX = r'Tiled Diffusion: \{.*},'
11 | REGION_REGEX = r'"Region [0-9][^}]*}'
12 | REGION_CONTROL_REGEX = r'(?<="Region control": ).*$'
13 | CONTROL_NET_REGEX = r'(ControlNet.*"[^"]*",)'
14 | CFG_REGEX = r' CFG Scheduler Info: ".*",'
15 | HYPHENED_STR_REGEX = r'\"[^"]*"'
16 |
17 |
18 | class ChunkData:
19 | def __init__(self, data, filepath=None, filetype=None, size=None):
20 | self.filepath = filepath
21 | self.type = filetype
22 | self.size = size
23 |
24 | self.data = data
25 | self.original_data = data
26 |
27 | self.data_list = []
28 | self.error_list = []
29 |
30 | self.params = {}
31 | self.used_params = {}
32 |
33 | def init_class(self):
34 | if not self.data:
35 | self.data = 'This file has no embedded data'
36 |
37 | if self.type == 0:
38 | ext = 'PNG'
39 | elif self.type == 1:
40 | ext = 'JPEG'
41 | elif self.type == 2:
42 | ext = 'WEBP'
43 | elif self.type == 9:
44 | ext = 'JSON'
45 | else:
46 | ext = '---'
47 |
48 | filename = os.path.basename(self.filepath)
49 |
50 | self.data_list.extend([['Filename', filename],
51 | ['Filepath', self.filepath],
52 | ['Extensions', ext],
53 | ['Image size', self.size]])
54 |
55 | if os.path.exists(self.filepath):
56 | timestamp = datetime.datetime.fromtimestamp(os.path.getctime(self.filepath))
57 | self.data_list.append(['Timestamp', timestamp.strftime('%Y/%m/%d %H:%M')])
58 |
59 | self.prompt_parse()
60 |
61 | if 'Tiled Diffusion' in self.data:
62 | self.tiled_diffusion_parse()
63 |
64 | if 'Lora' in self.data:
65 | self.lora_parse()
66 |
67 | if 'TI' in self.data:
68 | self.ti_parse()
69 |
70 | if 'ControlNet' in self.data:
71 | self.control_net_parse()
72 |
73 | if 'CFG Scheduler Info' in self.data:
74 | self.cfg_scheduler_parse()
75 |
76 | self.main_status_parse()
77 | self.make_dictionary()
78 |
79 | def data_refresh(self, delete_target, add_list):
80 | if delete_target:
81 | self.data = self.data.replace(delete_target, '')
82 |
83 | if add_list is not None and len(add_list) > 0:
84 | for index, value in enumerate(add_list):
85 | if len(value) == 2:
86 | self.data_list.append(value)
87 | else:
88 | self.error_list.append(value)
89 |
90 | def make_dictionary(self):
91 | add_net = 0
92 | extras = False
93 | cfg_auto = False
94 |
95 | if not self.data_list:
96 | return None
97 |
98 | self.data_list = [[value.strip() for value in d1] for d1 in self.data_list]
99 | for tmp in self.data_list:
100 | key, value = tmp
101 |
102 | if key == 'Tiled Diffusion scale factor' or key == 'Tiled Diffusion upscaler':
103 | continue
104 | elif 'AddNet Module' in key:
105 | add_net += 1
106 | elif 'Postprocess' in key:
107 | extras += 1
108 | elif key == 'Scheduler':
109 | cfg_auto += 1
110 |
111 | if value == 'true':
112 | value = 'True'
113 |
114 | self.params[key] = value
115 |
116 | if add_net > 0:
117 | self.params['AddNet Number'] = str(add_net)
118 |
119 | if extras:
120 | self.params['Extras'] = 'True'
121 |
122 | if cfg_auto:
123 | self.params['CFG auto'] = 'True'
124 |
125 | def model_name(self, model_list):
126 | model_hash = self.params.get('Model hash')
127 | if model_hash:
128 | model_name = '[' + model_hash + ']'
129 | if model_list:
130 | for tmp in model_list:
131 | if model_hash in tmp[1]:
132 | model_name = tmp[0] + ' [' + tmp[1] + ']'
133 |
134 | self.params['Model'] = model_name
135 | self.used_params['Model hash'] = True
136 |
137 | def vae_name(self, model_list):
138 | vae_hash = self.params.get('VAE hash')
139 | if vae_hash:
140 | vae_name = '[' + vae_hash + ']'
141 | if model_list:
142 | for tmp in model_list:
143 | if vae_hash in tmp[1]:
144 | vae_name = tmp[0] + ' [' + tmp[1] + ']'
145 |
146 | self.params['VAE'] = vae_name
147 | self.used_params['VAE hash'] = True
148 |
149 | def override_lora(self, model_list):
150 | for key, value in self.params.items():
151 | if 'Lora ' in key and '[' in value:
152 | match = re.search(r'\[.*]', value)
153 | if match:
154 | lora_hash = match.group().replace('[', '').replace(']', '')
155 | for tmp in model_list:
156 | if lora_hash in tmp[1]:
157 | self.params[key] = tmp[0] + ' [' + lora_hash + ']'
158 |
159 | def override_addnet_model(self, model_list):
160 | for key, value in self.params.items():
161 | if 'AddNet Model' in key:
162 | match = re.search(r'\(.*\)', value)
163 | if match:
164 | lora_hash = match.group().replace('(', '').replace(')', '')
165 | for tmp in model_list:
166 | if lora_hash in tmp[1]:
167 | self.params[key] = tmp[0] + ' (' + lora_hash + ')'
168 |
169 | def override_textual_inversion(self, model_list):
170 | for key, value in self.params.items():
171 | if 'Ti ' in key and '[' in value:
172 | match = re.search(r'\[.*]', value)
173 | if match:
174 | ti_hash = match.group().replace('[', '').replace(']', '')
175 | for tmp in model_list:
176 | if tmp[1] in ti_hash:
177 | self.params[key] = tmp[0] + ' [' + ti_hash + ']'
178 |
179 | def set_used_to_param_key(self, key):
180 | if key in self.params:
181 | self.used_params[key] = True
182 |
183 | def import_json(self, json_data):
184 | self.params = json_data
185 | self.used_params['Model hash'] = True
186 |
187 | def prompt_parse(self):
188 | result = [['Positive', 'None'], ['Negative', 'None']]
189 | if self.data == 'This file has no embedded data':
190 | result = [['Positive', self.data]]
191 | self.data_refresh(self.data, result)
192 | return
193 |
194 | match = re.search(PROMPT_REGEX, self.data)
195 | if match:
196 | prompt = match.group()
197 | matched_prompt = prompt
198 |
199 | if re.search(r'^parameters', prompt):
200 | matched_prompt = prompt.replace('parameters', '', 1)
201 | elif re.search(r'^UNICODE', prompt):
202 | matched_prompt = prompt.replace('UNICODE', '', 1)
203 |
204 | tmp = matched_prompt.split('Negative prompt: ')
205 | result[0][1] = tmp[0]
206 |
207 | if len(tmp) == 2:
208 | result[1][1] = tmp[1]
209 |
210 | self.data_refresh(prompt, result)
211 | else:
212 | match = re.search(r'^parameters', self.data)
213 | if match:
214 | matched_prompt = match.group()
215 | matched_prompt = matched_prompt.replace('parameters', '', 1)
216 | self.data_refresh('parameters', matched_prompt)
217 | else:
218 | unicode_match = re.search(r'^UNICODE', self.data)
219 | if unicode_match:
220 | matched_prompt = unicode_match.group()
221 | matched_prompt = matched_prompt.replace('UNICODE', '', 1)
222 | self.data_refresh('UNICODE', matched_prompt)
223 |
224 | def lora_parse(self):
225 | match = re.search(LORA_HASH_REGEX, self.data)
226 | if match:
227 | target = match.group().replace('"', '')
228 | loras = [d1.split(':')[0].strip() + ' ' + '[' + d1.split(':')[1].strip() + ']' for d1 in target.split(',')]
229 | loras = [['Lora ' + str(index), value] for index, value in enumerate(loras)]
230 | loras.append(['Lora', str(len(loras))])
231 | self.data_refresh('Lora hashes: "' + target + '",', loras)
232 |
233 | def ti_parse(self):
234 | match = re.search(TI_HASH_REGEX, self.data)
235 | if match:
236 | target = match.group().replace('"', '')
237 | tis = [d1.split(':')[0].strip() + ' ' + '[' + d1.split(':')[1].strip() + ']' for d1 in target.split(',')]
238 | tis = [['Ti ' + str(index), value] for index, value in enumerate(tis)]
239 | tis.append(['Textual inversion', str(len(tis))])
240 | self.data_refresh('TI hashes: "' + target + '",', tis)
241 |
242 | def tiled_diffusion_parse(self):
243 | region_status_list = []
244 | match = re.search(TILED_DIFFUSION_REGEX, self.data)
245 | if match:
246 | tiled_diffusion_status = match.group()
247 |
248 | if 'Region' in tiled_diffusion_status:
249 | region_status = re.findall(REGION_REGEX, tiled_diffusion_status)
250 | tiled_diffusion_status = re.sub(REGION_CONTROL_REGEX, 'True', tiled_diffusion_status)
251 |
252 | for tmp in region_status:
253 | tmp = re.sub(HYPHENED_STR_REGEX, lambda match_part: match_part.group().replace(',', ''), tmp)
254 | tmp = re.sub(HYPHENED_STR_REGEX, lambda match_part: match_part.group().replace(':', ''), tmp)
255 | number = tmp.split(':')[0]
256 | target_str = tmp.replace(number + ':', '').replace('{', '').replace('}', '').replace('"', '')
257 | target = target_str.split(',')
258 | number = number.replace('"', '')
259 | target = [[number + item.split(':')[0].replace('_', ' '), item.split(':')[1]] for item in target]
260 | region_status_list += target
261 |
262 | region_status_list.append(['Region control number', str(len(region_status))])
263 |
264 | tiled_diffusion_status = tiled_diffusion_status.replace('Tiled Diffusion: {', 'Tiled diffusion: True, ')
265 | tiled_diffusion_status = tiled_diffusion_status.replace('Tile tile', 'Tile')
266 | tiled_diffusion_status = tiled_diffusion_status.replace('"', '').replace('}', '')
267 | result = [item.split(':') for item in tiled_diffusion_status.split(',')]
268 |
269 | if region_status_list:
270 | result += region_status_list
271 |
272 | result = [[d2.replace('', ',').strip() for d2 in d1] for d1 in result]
273 | result = [[d2.replace('', ':').strip() for d2 in d1] for d1 in result]
274 | result = [[d2.replace('NoiseInv', 'Noise inversion') for d2 in d1] for d1 in result]
275 | result = [d1 for d1 in result if any(d1)]
276 | self.data_refresh(match.group(), result)
277 |
278 | def control_net_parse(self):
279 | match = re.search(CONTROL_NET_REGEX, self.data)
280 | if match:
281 | cnt = 0
282 | result = []
283 | target = match.group()
284 | controlnet_result = re.finditer(r'(ControlNet[^:]*: "[^"]*")', target)
285 |
286 | for tmp in controlnet_result:
287 | number = tmp.group().split(':')[0]
288 | hyphened = re.sub(r'(\([^)]*\))', lambda match_part: match_part.group(0).replace(', ', ''), tmp.group(0))
289 | detail_param = re.sub(r'(["|,][^:]*: )', lambda match_part: match_part.group(0).replace(',', ', ' + number), hyphened)
290 | detail_param = detail_param.replace(number + ':', number + ': True,' + number)
291 | detail_param = detail_param.replace('"', '')
292 | result = [[value.split(':')[0], value.split(':')[1]] for value in detail_param.split(',')]
293 | result = [[value.replace('', ',') for value in d1] for d1 in result]
294 | cnt += 1
295 |
296 | result.append(['ControlNet', str(cnt)])
297 | self.data_refresh(target, result)
298 |
299 | def cfg_scheduler_parse(self):
300 | match = re.search(CFG_REGEX, self.data)
301 | if match:
302 | target = match.group()
303 | cfg_match = re.search(HYPHENED_STR_REGEX, target)
304 | if cfg_match:
305 | cfg_result = cfg_match.group()
306 | cfg_result = cfg_result.replace('terget denoising', '\\n target denoising')
307 | cfg_result = cfg_result.replace('\\n"', '').replace('"', '')
308 | result = [[value.split(':')[0], str(value.split(':', 1)[1])] for value in cfg_result.split('\\n')]
309 | result.append(['CFG scheduler', 'True'])
310 | self.data_refresh(target, result)
311 |
312 | def main_status_parse(self):
313 | if self.data == 'This file has no embedded data':
314 | return
315 |
316 | if self.data:
317 | target_str = re.sub(HYPHENED_STR_REGEX, lambda match: match.group().replace(',', ''), self.data)
318 | noise_match = re.search(r'\nTemplate:[\s\S]*$', self.data)
319 |
320 | if noise_match:
321 | target_str = target_str.replace(noise_match.group(), '')
322 |
323 | if ':' in target_str:
324 | result = [[value.split(':')[0], value.split(':')[1]] for value in target_str.split(',')]
325 | result = [[d2.replace('', ',').replace('"', '').strip() for d2 in d1] for d1 in result]
326 | else:
327 | result = [target_str]
328 |
329 | self.data_refresh(target_str, result)
330 |
--------------------------------------------------------------------------------
/pyPromptChecker/lora/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from .interrogate import interrogate, model_downloads
4 |
--------------------------------------------------------------------------------
/pyPromptChecker/lora/interrogate.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import sys
3 |
4 | from PIL import Image
5 | from onnxruntime import InferenceSession
6 | from huggingface_hub.file_download import hf_hub_download
7 |
8 | import csv
9 | import PIL.Image
10 | import cv2
11 | import functools
12 | import os
13 | import numpy as np
14 |
15 |
16 | def make_square(img, target_size):
17 | old_size = img.shape[:2]
18 | desired_size = max(old_size)
19 | desired_size = max(desired_size, target_size)
20 |
21 | delta_w = desired_size - old_size[1]
22 | delta_h = desired_size - old_size[0]
23 | top, bottom = delta_h // 2, delta_h - (delta_h // 2)
24 | left, right = delta_w // 2, delta_w - (delta_w // 2)
25 |
26 | color = [255, 255, 255]
27 | new_im = cv2.copyMakeBorder(
28 | img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color
29 | )
30 | return new_im
31 |
32 |
33 | def smart_resize(img, size):
34 | if img.shape[0] > size:
35 | img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA)
36 | elif img.shape[0] < size:
37 | img = cv2.resize(img, (size, size), interpolation=cv2.INTER_CUBIC)
38 | return img
39 |
40 |
41 | def model_downloads(repository, filename, label_file, model_path):
42 | original = sys.stderr
43 | error_log = 'error.log'
44 | try:
45 | with open(error_log, 'w') as log:
46 | sys.stderr = log
47 | os.makedirs(model_path, exist_ok=True)
48 | hf_hub_download(repository, label_file, local_dir=model_path, local_dir_use_symlinks=False, use_auth_token=False)
49 | hf_hub_download(repository, filename, local_dir=model_path, local_dir_use_symlinks=False, use_auth_token=False)
50 | except Exception as e:
51 | return e
52 | else:
53 | os.remove(error_log)
54 | finally:
55 | sys.stderr = original
56 |
57 |
58 | def model_loads(model_path):
59 | if not os.path.exists(model_path):
60 | print('Error')
61 | return None
62 |
63 | loaded_model = InferenceSession(model_path, providers=['CPUExecutionProvider'])
64 | return loaded_model
65 |
66 |
67 | def label_loads(model_path, filename):
68 | label = os.path.join(model_path, filename)
69 |
70 | if not os.path.exists(label):
71 | print('Error')
72 | return None
73 |
74 | tags = []
75 | with open(label, "r") as f:
76 | reader = csv.reader(f)
77 | for row in reader:
78 | tags.append(row)
79 |
80 | tags_header = tags[0]
81 | category_index = tags_header.index('category')
82 | name_index = tags_header.index('name')
83 |
84 | names = [row[name_index] for row in tags[1:]]
85 | rating_indexes = [i for i, row in enumerate(tags[1:]) if row[category_index] == '9']
86 | general_indexes = [i for i, row in enumerate(tags[1:]) if row[category_index] == '0']
87 | chara_indexes = [i for i, row in enumerate(tags[1:]) if row[category_index] == '4']
88 |
89 | return names, rating_indexes, general_indexes, chara_indexes
90 |
91 |
92 | def predict(image,
93 | general_threshold,
94 | character_threshold,
95 | filename,
96 | tag_names,
97 | rating_indexes,
98 | general_indexes,
99 | character_indexes
100 | ):
101 | model = model_loads(filename)
102 | _, height, width, _ = model.get_inputs()[0].shape
103 |
104 | # Alpha to white
105 | image = image.convert("RGBA")
106 | new_image = PIL.Image.new("RGBA", image.size, "WHITE")
107 | new_image.paste(image, mask=image)
108 | image = new_image.convert("RGB")
109 | image = np.asarray(image)
110 |
111 | # PIL RGB to OpenCV BGR
112 | image = image[:, :, ::-1]
113 |
114 | image = make_square(image, height)
115 | image = smart_resize(image, height)
116 | image = image.astype(np.float32)
117 | image = np.expand_dims(image, 0)
118 |
119 | input_name = model.get_inputs()[0].name
120 | label_name = model.get_outputs()[0].name
121 | probs = model.run([label_name], {input_name: image})[0]
122 |
123 | labels = list(zip(tag_names, probs[0].astype(float)))
124 |
125 | # First 4 labels are actually ratings: pick one with argmax
126 | ratings_names = [labels[i] for i in rating_indexes]
127 | rating = dict(ratings_names)
128 |
129 | # Then we have general tags: pick anywhere prediction confidence > threshold
130 | general_names = [labels[i] for i in general_indexes]
131 | general_res = [x for x in general_names if x[1] > general_threshold]
132 | general_res = dict(general_res)
133 |
134 | # Everything else is characters: pick anywhere prediction confidence > threshold
135 | character_names = [labels[i] for i in character_indexes]
136 | character_res = [x for x in character_names if x[1] > character_threshold]
137 | character_res = dict(character_res)
138 |
139 | b = dict(sorted(general_res.items(), key=lambda item: item[1], reverse=True))
140 | a = (", ".join(list(b.keys())).replace("_", " ").replace("(", "\(").replace(")", "\)"))
141 | c = ", ".join(list(b.keys()))
142 |
143 | return a, c, rating, character_res, general_res
144 |
145 |
146 | def interrogate(model_param: str, filepath: str, tag_threshold: float, chara_threshold: float, installed: str):
147 | model_param = model_param.lower()
148 | model_filename = 'model.onnx'
149 | label_filename = "selected_tags.csv"
150 | model_path = os.path.join(os.path.abspath(installed), '.models/' + model_param)
151 |
152 | tag_names, rating_indexes, general_indexes, character_indexes = label_loads(model_path, label_filename)
153 |
154 | func = functools.partial(
155 | predict,
156 | filename=os.path.join(model_path, model_filename),
157 | tag_names=tag_names,
158 | rating_indexes=rating_indexes,
159 | general_indexes=general_indexes,
160 | character_indexes=character_indexes,
161 | )
162 |
163 | image_file = Image.open(filepath)
164 | prompt, original, rating, character, confidence = func(image_file, tag_threshold, chara_threshold)
165 | result = [filepath, model_param, tag_threshold, chara_threshold, prompt, original, rating, character, confidence]
166 |
167 | return result
168 |
--------------------------------------------------------------------------------
/pyPromptChecker/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import sys
4 | import argparse
5 |
6 | from gui import config
7 | from window import from_main, check_files, find_target
8 |
9 |
10 | def main():
11 | description_text = 'Script for extracting and formatting PNG chunks.\n'
12 | description_text = description_text + 'If no options are specified, the script will open a file choose dialog.\n'
13 | description_text = description_text + 'All options are mutually exclusive.'
14 | parser = argparse.ArgumentParser(description=description_text, formatter_class=argparse.RawTextHelpFormatter)
15 | group = parser.add_mutually_exclusive_group()
16 | group.add_argument('-a', '--ask', action='store_true', help='Open directory choose dialog.')
17 | parser.add_argument('filepaths', metavar='Filepath', type=str, nargs='*', help='Send path to files and directories.')
18 | args = parser.parse_args()
19 | filepaths = []
20 |
21 | if args.filepaths:
22 | parameters = args.filepaths
23 | elif args.ask:
24 | parameters = from_main('directory')
25 | else:
26 | parameters = from_main('files')
27 |
28 | if parameters:
29 | depth = config.get('SubDirectoryDepth', 0)
30 | filepaths = find_target(parameters, depth)
31 |
32 | if filepaths:
33 | valid_filepath, not_found_list, directory_list, not_png_list = check_files(filepaths)
34 | if not_found_list:
35 | print('\n'.join(not_found_list))
36 | print('These files are not found')
37 | if directory_list:
38 | print('\n'.join(directory_list))
39 | print('This is directory')
40 | if not_png_list:
41 | print('\n'.join(not_png_list))
42 | print('These files are not supported image files.')
43 | if not valid_filepath:
44 | print('There is no valid file to parse')
45 | sys.exit()
46 | print('a hoy!!!!')
47 | valid_filepath.sort()
48 |
49 | from_main('window', valid_filepath)
50 |
51 | else:
52 | print('Cancelled!')
53 |
54 |
55 | if __name__ == '__main__':
56 | main()
57 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name='pyPromptChecker',
5 | version='2.2.0',
6 | discription=' A small script for AI images created by stable diffusion webui ',
7 | author='Yui-tan',
8 | packages=find_packages(where='pyPromptChecker'),
9 | package_dir={'': 'pyPromptChecker'},
10 | install_requires=[
11 | 'pyQt6',
12 | 'pypng',
13 | 'pillow',
14 | 'pyqtdarktheme',
15 | 'onnxruntime',
16 | 'numpy',
17 | 'opencv-python',
18 | 'huggingface_hub'
19 | ],
20 | entry_points={
21 | 'console_scripts': [
22 | 'mikkumiku = main:main'
23 | ]
24 | }
25 | )
26 |
--------------------------------------------------------------------------------