├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .idea ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── pyPromptChecker.iml └── vcs.xml ├── config.ini ├── docs ├── LICENSE ├── README.md └── description.md ├── install ├── ebuild │ └── media-gfx │ │ └── pyPromptChecker │ │ ├── pyPromptChecker-1.4.0.ebuild │ │ └── pyPromptChecker-9999.ebuild ├── install.ps1 └── install.sh ├── model_list.csv ├── pyPromptChecker ├── __init__.py ├── gui │ ├── __init__.py │ ├── custom.py │ ├── dialog.py │ ├── listview.py │ ├── menu.py │ ├── search.py │ ├── tab.py │ ├── thumbnail.py │ ├── viewer.py │ └── widget.py ├── icon │ ├── icon.ico │ └── icon.png ├── lib │ ├── __init__.py │ ├── decoder.py │ ├── io.py │ └── parser.py ├── lora │ ├── __init__.py │ └── interrogate.py ├── main.py └── window.py └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Windows] 28 | - Scripts Version [e.g. v1.1.1-beta] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 53 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | ![Main](https://user-images.githubusercontent.com/121333129/270104343-eebf6863-d429-4233-ac82-24ff9d041aab.png) 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 | ![tab_navigation](https://user-images.githubusercontent.com/121333129/263465639-d02bc716-bfe9-4940-a655-6f8cade02348.png) 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 | ![tab_search](https://user-images.githubusercontent.com/121333129/266839621-7dd517b9-4a41-426b-ac42-d1e83e2045f9.png) 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 | ![compare](https://user-images.githubusercontent.com/121333129/263465633-7bda6efe-f70a-445a-b1ae-2436b41a7e15.png) 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 | ![main_screen](https://user-images.githubusercontent.com/121333129/261905025-69283ba2-ac9d-4b92-944e-bd187ce2abc2.png) 290 | ![main_screen_2](https://user-images.githubusercontent.com/121333129/261905120-ad750fda-e8b3-458d-b03a-a3d97eff7bfb.png) 291 | - ### Highres fix and Extras 292 | ![hires_extras](https://user-images.githubusercontent.com/121333129/263465147-63e51453-50c6-4c5c-949a-0069dc0dd5b4.png) 293 | - ### Lora and Add network 294 | ![Lora_addnet](https://user-images.githubusercontent.com/121333129/261911864-42731ce9-b9de-48b2-8f70-b538be7a56e8.png) 295 | - ### CFG 296 | ![CFG](https://user-images.githubusercontent.com/121333129/263465167-0875b003-f0c2-4ed2-a876-f7bd7e9df138.png) 297 | - ### Tiled diffusion 298 | ![tiled_diffusion](https://user-images.githubusercontent.com/121333129/261911802-a571772a-3e53-404b-b09b-4b9dc576add9.png) 299 | - ### Controlnet 300 | ![controlnet](https://user-images.githubusercontent.com/121333129/261911911-cb4219a6-0270-4381-ba59-b333a91d7456.png) 301 | - ### Regional prompter 302 | ![regional_prompter](https://user-images.githubusercontent.com/121333129/261966851-dff68376-70e2-4fe9-a24b-399c120e0f60.png) 303 | ![regional_prompter_2](https://user-images.githubusercontent.com/121333129/261966907-8cb29c70-1c9a-4601-98cc-8c771e5cf608.png) 304 | - ### Region control in Tiled diffusion 305 | - ### Image view 306 | ![image_view](https://user-images.githubusercontent.com/121333129/261905238-2aee6631-de09-4a1a-9052-a61bba7f348a.png) 307 | - ### Thumbnail tab navigation 308 | ![Thumbnail](https://user-images.githubusercontent.com/121333129/261905360-cae29606-c641-4400-9c5d-64bb5251d8af.png) 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 | --------------------------------------------------------------------------------