justineightyone commited on
Commit
48ee475
·
verified ·
1 Parent(s): 4553f02

Initial release: JoyCaption Beta One + JoyTag batch captioning WebUI

Browse files
.gitattributes CHANGED
@@ -1,35 +1,3 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ * text=auto
2
+ *.bat text eol=crlf
3
+ *.sh text eol=lf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ venv/
2
+ models/
3
+ __pycache__/
4
+ *.pyc
5
+ .claude/
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
app.py ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ JoyCaption Batch WebUI — local Gradio UI for JoyCaption Beta One + JoyTag,
3
+ with batch dataset captioning (writes .txt files next to images, kohya-style).
4
+
5
+ Models by fancyfeast (fpgaminer): https://github.com/fpgaminer/joycaption (Apache-2.0)
6
+ UI based on the HF spaces fancyfeast/joy-caption-beta-one and MegaTronX/JoyCaptionBetaOne.
7
+
8
+ Defaults target a 24GB GPU (bf16 + SDPA + TF32); run with --nf4 for 4-bit
9
+ (~7GB VRAM) if you need to keep a diffusion UI like ComfyUI loaded alongside.
10
+ """
11
+
12
+ import argparse
13
+ import gc
14
+ import os
15
+ from pathlib import Path
16
+ from threading import Thread
17
+ from typing import Generator
18
+
19
+ import torch
20
+ import gradio as gr
21
+ from PIL import Image
22
+ from transformers import LlavaForConditionalGeneration, TextIteratorStreamer, AutoProcessor
23
+
24
+ parser = argparse.ArgumentParser()
25
+ parser.add_argument("--nf4", action="store_true", help="Load model in 4-bit NF4 (~7GB VRAM instead of ~18GB)")
26
+ parser.add_argument("--port", type=int, default=7861, help="Port (default 7861, avoids ComfyUI's 8188/7860)")
27
+ parser.add_argument("--listen", action="store_true", help="Listen on 0.0.0.0 (LAN access)")
28
+ parser.add_argument("--cache-dir", type=str, default=None, help="Model download/cache dir (default: ./models next to this file)")
29
+ parser.add_argument("--no-browser", action="store_true", help="Don't auto-open a browser tab on launch")
30
+ args = parser.parse_args()
31
+
32
+ ON_SPACES = os.environ.get("SPACE_ID") is not None
33
+ DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"
34
+
35
+ MODEL_PATH = "fancyfeast/llama-joycaption-beta-one-hf-llava"
36
+ CACHE_DIR = args.cache_dir or str(Path(__file__).parent / "models")
37
+
38
+ # Ampere+ optimizations
39
+ torch.backends.cuda.matmul.allow_tf32 = True
40
+ torch.backends.cudnn.allow_tf32 = True
41
+
42
+ TITLE = """<h1 style="text-align:center;">JoyCaption Batch WebUI</h1>
43
+ <p style="text-align:center;">JoyCaption Beta One + JoyTag &mdash; single-image and batch dataset captioning</p>
44
+ <hr>"""
45
+
46
+ CAPTION_TYPE_MAP = {
47
+ "Descriptive": [
48
+ "Write a detailed description for this image.",
49
+ "Write a detailed description for this image in {word_count} words or less.",
50
+ "Write a {length} detailed description for this image.",
51
+ ],
52
+ "Descriptive (Casual)": [
53
+ "Write a descriptive caption for this image in a casual tone.",
54
+ "Write a descriptive caption for this image in a casual tone within {word_count} words.",
55
+ "Write a {length} descriptive caption for this image in a casual tone.",
56
+ ],
57
+ "Straightforward": [
58
+ "Write a straightforward caption for this image. Begin with the main subject and medium. Mention pivotal elements—people, objects, scenery—using confident, definite language. Focus on concrete details like color, shape, texture, and spatial relationships. Show how elements interact. Omit mood and speculative wording. If text is present, quote it exactly. Note any watermarks, signatures, or compression artifacts. Never mention what's absent, resolution, or unobservable details. Vary your sentence structure and keep the description concise, without starting with \"This image is…\" or similar phrasing.",
59
+ "Write a straightforward caption for this image within {word_count} words. Begin with the main subject and medium. Mention pivotal elements—people, objects, scenery—using confident, definite language. Focus on concrete details like color, shape, texture, and spatial relationships. Show how elements interact. Omit mood and speculative wording. If text is present, quote it exactly. Note any watermarks, signatures, or compression artifacts. Never mention what's absent, resolution, or unobservable details. Vary your sentence structure and keep the description concise, without starting with \"This image is…\" or similar phrasing.",
60
+ "Write a {length} straightforward caption for this image. Begin with the main subject and medium. Mention pivotal elements—people, objects, scenery—using confident, definite language. Focus on concrete details like color, shape, texture, and spatial relationships. Show how elements interact. Omit mood and speculative wording. If text is present, quote it exactly. Note any watermarks, signatures, or compression artifacts. Never mention what's absent, resolution, or unobservable details. Vary your sentence structure and keep the description concise, without starting with \"This image is…\" or similar phrasing.",
61
+ ],
62
+ "Stable Diffusion Prompt": [
63
+ "Output a stable diffusion prompt that is indistinguishable from a real stable diffusion prompt.",
64
+ "Output a stable diffusion prompt that is indistinguishable from a real stable diffusion prompt. {word_count} words or less.",
65
+ "Output a {length} stable diffusion prompt that is indistinguishable from a real stable diffusion prompt.",
66
+ ],
67
+ "MidJourney": [
68
+ "Write a MidJourney prompt for this image.",
69
+ "Write a MidJourney prompt for this image within {word_count} words.",
70
+ "Write a {length} MidJourney prompt for this image.",
71
+ ],
72
+ "Danbooru tag list": [
73
+ "Generate only comma-separated Danbooru tags (lowercase_underscores). Strict order: `artist:`, `copyright:`, `character:`, `meta:`, then general tags. Include counts (1girl), appearance, clothing, accessories, pose, expression, actions, background. Use precise Danbooru syntax. No extra text.",
74
+ "Generate only comma-separated Danbooru tags (lowercase_underscores). Strict order: `artist:`, `copyright:`, `character:`, `meta:`, then general tags. Include counts (1girl), appearance, clothing, accessories, pose, expression, actions, background. Use precise Danbooru syntax. No extra text. {word_count} words or less.",
75
+ "Generate only comma-separated Danbooru tags (lowercase_underscores). Strict order: `artist:`, `copyright:`, `character:`, `meta:`, then general tags. Include counts (1girl), appearance, clothing, accessories, pose, expression, actions, background. Use precise Danbooru syntax. No extra text. {length} length.",
76
+ ],
77
+ "e621 tag list": [
78
+ "Write a comma-separated list of e621 tags in alphabetical order for this image. Start with the artist, copyright, character, species, meta, and lore tags (if any), prefixed by 'artist:', 'copyright:', 'character:', 'species:', 'meta:', and 'lore:'. Then all the general tags.",
79
+ "Write a comma-separated list of e621 tags in alphabetical order for this image. Start with the artist, copyright, character, species, meta, and lore tags (if any), prefixed by 'artist:', 'copyright:', 'character:', 'species:', 'meta:', and 'lore:'. Then all the general tags. Keep it under {word_count} words.",
80
+ "Write a {length} comma-separated list of e621 tags in alphabetical order for this image. Start with the artist, copyright, character, species, meta, and lore tags (if any), prefixed by 'artist:', 'copyright:', 'character:', 'species:', 'meta:', and 'lore:'. Then all the general tags.",
81
+ ],
82
+ "Rule34 tag list": [
83
+ "Write a comma-separated list of rule34 tags in alphabetical order for this image. Start with the artist, copyright, character, and meta tags (if any), prefixed by 'artist:', 'copyright:', 'character:', and 'meta:'. Then all the general tags.",
84
+ "Write a comma-separated list of rule34 tags in alphabetical order for this image. Start with the artist, copyright, character, and meta tags (if any), prefixed by 'artist:', 'copyright:', 'character:', and 'meta:'. Then all the general tags. Keep it under {word_count} words.",
85
+ "Write a {length} comma-separated list of rule34 tags in alphabetical order for this image. Start with the artist, copyright, character, and meta tags (if any), prefixed by 'artist:', 'copyright:', 'character:', and 'meta:'. Then all the general tags.",
86
+ ],
87
+ "Booru-like tag list": [
88
+ "Write a list of Booru-like tags for this image.",
89
+ "Write a list of Booru-like tags for this image within {word_count} words.",
90
+ "Write a {length} list of Booru-like tags for this image.",
91
+ ],
92
+ "Art Critic": [
93
+ "Analyze this image like an art critic would with information about its composition, style, symbolism, the use of color, light, any artistic movement it might belong to, etc.",
94
+ "Analyze this image like an art critic would with information about its composition, style, symbolism, the use of color, light, any artistic movement it might belong to, etc. Keep it within {word_count} words.",
95
+ "Analyze this image like an art critic would with information about its composition, style, symbolism, the use of color, light, any artistic movement it might belong to, etc. Keep it {length}.",
96
+ ],
97
+ "Product Listing": [
98
+ "Write a caption for this image as though it were a product listing.",
99
+ "Write a caption for this image as though it were a product listing. Keep it under {word_count} words.",
100
+ "Write a {length} caption for this image as though it were a product listing.",
101
+ ],
102
+ "Social Media Post": [
103
+ "Write a caption for this image as if it were being used for a social media post.",
104
+ "Write a caption for this image as if it were being used for a social media post. Limit the caption to {word_count} words.",
105
+ "Write a {length} caption for this image as if it were being used for a social media post.",
106
+ ],
107
+ }
108
+
109
+ NAME_OPTION = "If there is a person/character in the image you must refer to them as {name}."
110
+
111
+ EXTRA_OPTIONS_LIST = [
112
+ "Do NOT include information about people/characters that cannot be changed (like ethnicity, gender, etc), but do still include changeable attributes (like hair style).",
113
+ "Include information about lighting.",
114
+ "Include information about camera angle.",
115
+ "Include information about whether there is a watermark or not.",
116
+ "Include information about whether there are JPEG artifacts or not.",
117
+ "If it is a photo you MUST include information about what camera was likely used and details such as aperture, shutter speed, ISO, etc.",
118
+ "Do NOT include anything sexual; keep it PG.",
119
+ "Do NOT mention the image's resolution.",
120
+ "You MUST include information about the subjective aesthetic quality of the image from low to very high.",
121
+ "Include information on the image's composition style, such as leading lines, rule of thirds, or symmetry.",
122
+ "Do NOT mention any text that is in the image.",
123
+ "Specify the depth of field and whether the background is in focus or blurred.",
124
+ "If applicable, mention the likely use of artificial or natural lighting sources.",
125
+ "Do NOT use any ambiguous language.",
126
+ "Include whether the image is sfw, suggestive, or nsfw.",
127
+ "ONLY describe the most important elements of the image.",
128
+ ]
129
+
130
+ IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
131
+
132
+ processor = None
133
+ model = None
134
+
135
+ JOYTAG_REPO = "fancyfeast/joytag"
136
+ joytag_model = None
137
+ joytag_tags = None
138
+
139
+
140
+ def load_model() -> str:
141
+ global processor, model
142
+ if model is not None:
143
+ return "✅ Model is already loaded."
144
+ print("Loading processor...")
145
+ processor = AutoProcessor.from_pretrained(MODEL_PATH, cache_dir=CACHE_DIR)
146
+ print("Loading model..." + (" (4-bit NF4)" if args.nf4 else " (bf16)"))
147
+ load_kwargs = dict(
148
+ torch_dtype=torch.bfloat16,
149
+ attn_implementation="sdpa",
150
+ cache_dir=CACHE_DIR,
151
+ )
152
+ if args.nf4:
153
+ from transformers import BitsAndBytesConfig
154
+ load_kwargs["quantization_config"] = BitsAndBytesConfig(
155
+ load_in_4bit=True,
156
+ bnb_4bit_quant_type="nf4",
157
+ bnb_4bit_compute_dtype=torch.bfloat16,
158
+ bnb_4bit_use_double_quant=True,
159
+ )
160
+ load_kwargs["device_map"] = DEVICE
161
+ else:
162
+ load_kwargs["device_map"] = DEVICE
163
+ model = LlavaForConditionalGeneration.from_pretrained(MODEL_PATH, **load_kwargs)
164
+ model.eval()
165
+ print("Model loaded and ready!")
166
+ return "✅ Model loaded and ready!" + (" (4-bit NF4)" if args.nf4 else " (bf16)")
167
+
168
+
169
+ def unload_model() -> str:
170
+ global processor, model
171
+ if model is None:
172
+ return "⚠️ Model is not currently loaded."
173
+ del model
174
+ del processor
175
+ model = None
176
+ processor = None
177
+ gc.collect()
178
+ torch.cuda.empty_cache()
179
+ return "✅ Model unloaded from VRAM."
180
+
181
+
182
+ def load_joytag() -> str:
183
+ global joytag_model, joytag_tags
184
+ if joytag_model is not None:
185
+ return "✅ JoyTag is already loaded."
186
+ import huggingface_hub
187
+ from joytag_models import VisionModel
188
+ print("Loading JoyTag...")
189
+ path = huggingface_hub.snapshot_download(JOYTAG_REPO, cache_dir=CACHE_DIR)
190
+ joytag_model = VisionModel.load_model(path, device="cuda" if torch.cuda.is_available() else "cpu")
191
+ joytag_model.eval()
192
+ joytag_tags = [line.strip() for line in (Path(path) / "top_tags.txt").read_text(encoding="utf-8").splitlines() if line.strip()]
193
+ print("JoyTag loaded!")
194
+ return "✅ JoyTag loaded (~0.4GB VRAM)."
195
+
196
+
197
+ def unload_joytag() -> str:
198
+ global joytag_model, joytag_tags
199
+ if joytag_model is None:
200
+ return "⚠️ JoyTag is not currently loaded."
201
+ joytag_model = None
202
+ joytag_tags = None
203
+ gc.collect()
204
+ torch.cuda.empty_cache()
205
+ return "✅ JoyTag unloaded from VRAM."
206
+
207
+
208
+ def prepare_joytag_image(image: Image.Image, target_size: int) -> torch.Tensor:
209
+ import torchvision.transforms.functional as TVF
210
+ # Pad to square on white, resize, CLIP-normalize (mirrors the official space)
211
+ max_dim = max(image.size)
212
+ padded = Image.new("RGB", (max_dim, max_dim), (255, 255, 255))
213
+ padded.paste(image, ((max_dim - image.size[0]) // 2, (max_dim - image.size[1]) // 2))
214
+ if max_dim != target_size:
215
+ padded = padded.resize((target_size, target_size), Image.BICUBIC)
216
+ tensor = TVF.pil_to_tensor(padded) / 255.0
217
+ return TVF.normalize(tensor, mean=[0.48145466, 0.4578275, 0.40821073], std=[0.26862954, 0.26130258, 0.27577711])
218
+
219
+
220
+ @torch.no_grad()
221
+ def joytag_predict(image: Image.Image, threshold: float) -> tuple[list[tuple[str, float]], dict[str, float]]:
222
+ """Returns (tags over threshold sorted by confidence, all scores)."""
223
+ if joytag_model is None:
224
+ load_joytag()
225
+ device = next(joytag_model.parameters()).device
226
+ tensor = prepare_joytag_image(image.convert("RGB"), joytag_model.image_size).unsqueeze(0).to(device)
227
+ with torch.autocast(device.type, enabled=True):
228
+ preds = joytag_model({"image": tensor})
229
+ tag_preds = preds["tags"].sigmoid().float().cpu()[0]
230
+ scores = {joytag_tags[i]: tag_preds[i].item() for i in range(len(joytag_tags))}
231
+ predicted = sorted(((t, s) for t, s in scores.items() if s > threshold), key=lambda x: -x[1])
232
+ return predicted, scores
233
+
234
+
235
+ def format_joytag_tags(predicted: list[tuple[str, float]], spaces: bool) -> str:
236
+ tags = [t for t, _ in predicted]
237
+ if spaces:
238
+ tags = [t.replace("_", " ") for t in tags]
239
+ return ", ".join(tags)
240
+
241
+
242
+ def joytag_single(input_image: Image.Image, threshold: float, spaces: bool):
243
+ if input_image is None:
244
+ return "Please upload an image first.", ""
245
+ predicted, scores = joytag_predict(input_image, threshold)
246
+ top = sorted(scores.items(), key=lambda x: -x[1])[:50]
247
+ score_text = "\n".join(f"{s:.3f} {t}" for t, s in top)
248
+ return format_joytag_tags(predicted, spaces), score_text
249
+
250
+
251
+ def joytag_batch(
252
+ folder: str,
253
+ threshold: float,
254
+ spaces: bool,
255
+ skip_existing: bool,
256
+ prefix: str,
257
+ progress=gr.Progress(),
258
+ ) -> Generator[str, None, None]:
259
+ folder_path = Path(folder.strip().strip('"'))
260
+ if not folder_path.is_dir():
261
+ yield f"❌ Not a folder: {folder_path}"
262
+ return
263
+ if joytag_model is None:
264
+ yield "Loading JoyTag..."
265
+ load_joytag()
266
+
267
+ images = sorted(p for p in folder_path.iterdir() if p.suffix.lower() in IMAGE_EXTS)
268
+ if not images:
269
+ yield f"❌ No images found in {folder_path}"
270
+ return
271
+
272
+ log = []
273
+ done = skipped = failed = 0
274
+ for i, img_path in enumerate(progress.tqdm(images, desc="Tagging")):
275
+ txt_path = img_path.with_suffix(".txt")
276
+ if skip_existing and txt_path.exists():
277
+ skipped += 1
278
+ continue
279
+ try:
280
+ predicted, _ = joytag_predict(Image.open(img_path), threshold)
281
+ tag_string = format_joytag_tags(predicted, spaces)
282
+ if prefix.strip():
283
+ tag_string = prefix.strip() + " " + tag_string
284
+ txt_path.write_text(tag_string, encoding="utf-8")
285
+ done += 1
286
+ log.append(f"[{i + 1}/{len(images)}] {img_path.name} ✓ ({len(predicted)} tags)")
287
+ except Exception as e:
288
+ failed += 1
289
+ log.append(f"[{i + 1}/{len(images)}] {img_path.name} ❌ {e}")
290
+ yield "\n".join(log[-30:]) + f"\n\n— {done} tagged, {skipped} skipped, {failed} failed —"
291
+
292
+ yield "\n".join(log[-30:]) + f"\n\n✅ Finished: {done} tagged, {skipped} skipped, {failed} failed ({len(images)} images total)"
293
+
294
+
295
+ def build_prompt(caption_type: str, caption_length: str | int, extra_options: list[str], name_input: str) -> str:
296
+ if caption_length == "any":
297
+ map_idx = 0
298
+ elif isinstance(caption_length, str) and caption_length.isdigit():
299
+ map_idx = 1 # numeric word count template
300
+ else:
301
+ map_idx = 2 # length descriptor template
302
+
303
+ prompt = CAPTION_TYPE_MAP[caption_type][map_idx]
304
+
305
+ if extra_options:
306
+ prompt += " " + " ".join(extra_options)
307
+
308
+ return prompt.format(name=name_input, length=caption_length, word_count=caption_length)
309
+
310
+
311
+ def toggle_name_box(extra_options: list[str]):
312
+ return gr.update(visible=NAME_OPTION in extra_options)
313
+
314
+
315
+ def prepare_inputs(input_image: Image.Image, prompt: str):
316
+ convo = [
317
+ {"role": "system", "content": "You are a helpful image captioner."},
318
+ {"role": "user", "content": prompt},
319
+ ]
320
+ convo_string = processor.apply_chat_template(convo, tokenize=False, add_generation_prompt=True)
321
+ inputs = processor(text=[convo_string], images=[input_image.convert("RGB")], return_tensors="pt").to(model.device)
322
+ inputs["pixel_values"] = inputs["pixel_values"].to(torch.bfloat16)
323
+ return inputs
324
+
325
+
326
+ def generation_kwargs(temperature: float, top_p: float, max_new_tokens: int) -> dict:
327
+ return dict(
328
+ max_new_tokens=int(max_new_tokens),
329
+ do_sample=temperature > 0,
330
+ suppress_tokens=None,
331
+ use_cache=True,
332
+ temperature=temperature if temperature > 0 else None,
333
+ top_k=None,
334
+ top_p=top_p if temperature > 0 else None,
335
+ )
336
+
337
+
338
+ @torch.no_grad()
339
+ def chat_joycaption(
340
+ input_image: Image.Image,
341
+ prompt: str,
342
+ temperature: float,
343
+ top_p: float,
344
+ max_new_tokens: int,
345
+ ) -> Generator[str, None, None]:
346
+ if input_image is None:
347
+ yield "Please upload an image first."
348
+ return
349
+ if model is None:
350
+ load_model()
351
+
352
+ torch.cuda.empty_cache()
353
+ inputs = prepare_inputs(input_image, prompt)
354
+
355
+ streamer = TextIteratorStreamer(processor.tokenizer, skip_prompt=True, skip_special_tokens=True)
356
+ gen_kwargs = dict(**inputs, streamer=streamer, **generation_kwargs(temperature, top_p, max_new_tokens))
357
+
358
+ thread = Thread(target=model.generate, kwargs=gen_kwargs)
359
+ thread.start()
360
+
361
+ output = ""
362
+ for new_text in streamer:
363
+ output += new_text
364
+ yield output.strip()
365
+
366
+ thread.join()
367
+
368
+
369
+ @torch.no_grad()
370
+ def caption_single(input_image: Image.Image, prompt: str, temperature: float, top_p: float, max_new_tokens: int) -> str:
371
+ inputs = prepare_inputs(input_image, prompt)
372
+ generate_ids = model.generate(**inputs, **generation_kwargs(temperature, top_p, max_new_tokens))[0]
373
+ generate_ids = generate_ids[inputs["input_ids"].shape[1]:]
374
+ return processor.tokenizer.decode(generate_ids, skip_special_tokens=True).strip()
375
+
376
+
377
+ def batch_caption(
378
+ folder: str,
379
+ prompt: str,
380
+ temperature: float,
381
+ top_p: float,
382
+ max_new_tokens: int,
383
+ skip_existing: bool,
384
+ prefix: str,
385
+ progress=gr.Progress(),
386
+ ) -> Generator[str, None, None]:
387
+ folder_path = Path(folder.strip().strip('"'))
388
+ if not folder_path.is_dir():
389
+ yield f"❌ Not a folder: {folder_path}"
390
+ return
391
+ if model is None:
392
+ yield "Loading model..."
393
+ load_model()
394
+
395
+ images = sorted(p for p in folder_path.iterdir() if p.suffix.lower() in IMAGE_EXTS)
396
+ if not images:
397
+ yield f"❌ No images found in {folder_path}"
398
+ return
399
+
400
+ log = []
401
+ done = skipped = failed = 0
402
+ for i, img_path in enumerate(progress.tqdm(images, desc="Captioning")):
403
+ txt_path = img_path.with_suffix(".txt")
404
+ if skip_existing and txt_path.exists():
405
+ skipped += 1
406
+ continue
407
+ try:
408
+ image = Image.open(img_path)
409
+ caption = caption_single(image, prompt, temperature, top_p, max_new_tokens)
410
+ if prefix.strip():
411
+ caption = prefix.strip() + " " + caption
412
+ txt_path.write_text(caption, encoding="utf-8")
413
+ done += 1
414
+ log.append(f"[{i + 1}/{len(images)}] {img_path.name} ✓")
415
+ except Exception as e:
416
+ failed += 1
417
+ log.append(f"[{i + 1}/{len(images)}] {img_path.name} ❌ {e}")
418
+ yield "\n".join(log[-30:]) + f"\n\n— {done} captioned, {skipped} skipped, {failed} failed —"
419
+
420
+ yield "\n".join(log[-30:]) + f"\n\n✅ Finished: {done} captioned, {skipped} skipped, {failed} failed ({len(images)} images total)"
421
+
422
+
423
+ # ─── Gradio UI ───────────────────────────────────────────────────────────────
424
+
425
+ # gradio 6.12's tab bar moves tabs that don't fit into an "overflow" dropdown, and
426
+ # selecting a tab from that dropdown hangs the page in a layout loop. Letting the tab
427
+ # bar wrap keeps scrollWidth <= clientWidth so the overflow logic never engages.
428
+ TAB_OVERFLOW_FIX_CSS = """
429
+ .tab-container { flex-wrap: wrap !important; }
430
+ """
431
+
432
+ with gr.Blocks(title="JoyCaption Batch WebUI", css=TAB_OVERFLOW_FIX_CSS) as demo:
433
+ gr.HTML(TITLE)
434
+
435
+ with gr.Row():
436
+ load_btn = gr.Button("⚡ Load Model", variant="primary")
437
+ unload_btn = gr.Button("🗑️ Unload Model from VRAM", variant="stop")
438
+ model_status = gr.Textbox(label="Model Status", value="Model loads automatically on first caption.", interactive=False)
439
+ load_btn.click(load_model, outputs=model_status)
440
+ unload_btn.click(unload_model, outputs=model_status)
441
+
442
+ # Shared prompt controls
443
+ with gr.Row():
444
+ with gr.Column():
445
+ caption_type = gr.Dropdown(
446
+ choices=list(CAPTION_TYPE_MAP.keys()),
447
+ label="Caption Type",
448
+ value="Descriptive",
449
+ )
450
+ caption_length = gr.Dropdown(
451
+ choices=["any", "very short", "short", "medium-length", "long", "very long"]
452
+ + [str(i) for i in range(20, 261, 10)],
453
+ label="Caption Length",
454
+ value="long",
455
+ )
456
+ with gr.Accordion("Extra Options", open=False):
457
+ extra_options = gr.CheckboxGroup(
458
+ choices=[NAME_OPTION] + EXTRA_OPTIONS_LIST,
459
+ label="Extra Options",
460
+ )
461
+ name_input = gr.Textbox(
462
+ label="Person/Character Name (if applicable)",
463
+ visible=False,
464
+ )
465
+ with gr.Accordion("Generation Settings", open=False):
466
+ temperature_slider = gr.Slider(minimum=0.0, maximum=2.0, value=0.6, step=0.05, label="Temperature")
467
+ top_p_slider = gr.Slider(minimum=0.0, maximum=1.0, value=0.9, step=0.01, label="Top-p")
468
+ max_tokens_slider = gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max New Tokens")
469
+
470
+ with gr.Column():
471
+ # Initial value set here directly: a demo.load() handler for this hangs the
472
+ # page when switching to a third tab (gradio 6.12 bug, see repro notes).
473
+ prompt_box = gr.Textbox(
474
+ lines=4, label="Prompt", interactive=True,
475
+ value=build_prompt("Descriptive", "long", [], ""),
476
+ )
477
+
478
+ extra_options.change(toggle_name_box, inputs=extra_options, outputs=name_input)
479
+ for ctrl in (caption_type, caption_length, extra_options, name_input):
480
+ ctrl.change(
481
+ build_prompt,
482
+ inputs=[caption_type, caption_length, extra_options, name_input],
483
+ outputs=prompt_box,
484
+ )
485
+
486
+ with gr.Tab("Single Image"):
487
+ with gr.Row():
488
+ with gr.Column():
489
+ input_image = gr.Image(type="pil", label="Input Image")
490
+ run_button = gr.Button("Caption", variant="primary")
491
+ with gr.Column():
492
+ output_caption = gr.Textbox(label="Caption", lines=10)
493
+ copy_btn = gr.Button("📋 Copy to Clipboard")
494
+ copy_btn.click(None, output_caption, None, js="(text) => navigator.clipboard.writeText(text)")
495
+
496
+ run_button.click(
497
+ chat_joycaption,
498
+ inputs=[input_image, prompt_box, temperature_slider, top_p_slider, max_tokens_slider],
499
+ outputs=output_caption,
500
+ )
501
+
502
+ with gr.Tab("Batch Folder"):
503
+ gr.Markdown("Dataset captioning: captions every image in a folder and writes a `.txt` with the same name next to each image (kohya/diffusion-pipe style).")
504
+ folder_box = gr.Textbox(label="Folder Path", placeholder=r"C:\path\to\my_dataset")
505
+ with gr.Row():
506
+ skip_existing_cb = gr.Checkbox(value=True, label="Skip images that already have a .txt")
507
+ prefix_box = gr.Textbox(label="Caption Prefix (optional, e.g. trigger word)", placeholder="myTriggerWord,")
508
+ batch_button = gr.Button("Caption Folder", variant="primary")
509
+ batch_log = gr.Textbox(label="Progress", lines=15)
510
+
511
+ batch_button.click(
512
+ batch_caption,
513
+ inputs=[folder_box, prompt_box, temperature_slider, top_p_slider, max_tokens_slider, skip_existing_cb, prefix_box],
514
+ outputs=batch_log,
515
+ )
516
+
517
+ with gr.Tab("JoyTag"):
518
+ gr.Markdown(
519
+ "[JoyTag](https://huggingface.co/fancyfeast/joytag) is a small ViT Danbooru tagger by the same author — "
520
+ "near-instant per image, ~0.4GB VRAM, loads independently of the JoyCaption model. "
521
+ "It loads automatically on first use."
522
+ )
523
+ with gr.Row():
524
+ joytag_load_btn = gr.Button("⚡ Load JoyTag")
525
+ joytag_unload_btn = gr.Button("🗑️ Unload JoyTag", variant="stop")
526
+ joytag_status = gr.Textbox(label="JoyTag Status", value="JoyTag loads automatically on first use.", interactive=False)
527
+ joytag_load_btn.click(load_joytag, outputs=joytag_status)
528
+ joytag_unload_btn.click(unload_joytag, outputs=joytag_status)
529
+
530
+ with gr.Row():
531
+ joytag_threshold = gr.Slider(minimum=0.05, maximum=0.95, value=0.4, step=0.05, label="Tag Threshold (lower = more tags)")
532
+ joytag_spaces_cb = gr.Checkbox(value=False, label="Replace underscores with spaces")
533
+
534
+ with gr.Row():
535
+ with gr.Column():
536
+ joytag_image = gr.Image(type="pil", label="Input Image")
537
+ joytag_run_btn = gr.Button("Tag", variant="primary")
538
+ with gr.Column():
539
+ joytag_output = gr.Textbox(label="Tags", lines=6)
540
+ # NOTE: gr.Label hangs the page on tab switch in gradio 6.12 — use a textbox
541
+ joytag_scores = gr.Textbox(label="Tag Confidences (top 50)", lines=10)
542
+ joytag_run_btn.click(
543
+ joytag_single,
544
+ inputs=[joytag_image, joytag_threshold, joytag_spaces_cb],
545
+ outputs=[joytag_output, joytag_scores],
546
+ )
547
+
548
+ with gr.Accordion("Batch Folder (dataset tagging)", open=False):
549
+ gr.Markdown("Tags every image in a folder and writes a `.txt` with the same name next to each image.")
550
+ joytag_folder_box = gr.Textbox(label="Folder Path", placeholder=r"C:\path\to\my_dataset")
551
+ with gr.Row():
552
+ joytag_skip_cb = gr.Checkbox(value=True, label="Skip images that already have a .txt")
553
+ joytag_prefix_box = gr.Textbox(label="Tag Prefix (optional, e.g. trigger word)", placeholder="myTriggerWord,")
554
+ joytag_batch_btn = gr.Button("Tag Folder", variant="primary")
555
+ joytag_batch_log = gr.Textbox(label="Progress", lines=15)
556
+ joytag_batch_btn.click(
557
+ joytag_batch,
558
+ inputs=[joytag_folder_box, joytag_threshold, joytag_spaces_cb, joytag_skip_cb, joytag_prefix_box],
559
+ outputs=joytag_batch_log,
560
+ )
561
+
562
+ if __name__ == "__main__":
563
+ if ON_SPACES:
564
+ # On HF Spaces let gradio pick up the Spaces defaults (0.0.0.0:7860).
565
+ demo.launch()
566
+ else:
567
+ demo.launch(
568
+ server_name="0.0.0.0" if args.listen else "127.0.0.1",
569
+ server_port=args.port,
570
+ inbrowser=not args.no_browser,
571
+ )
install_linux.sh ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # One-time setup: creates a venv and installs dependencies (CUDA 12.4 wheels).
3
+ set -e
4
+ cd "$(dirname "$0")"
5
+ python3 -m venv venv
6
+ source venv/bin/activate
7
+ python -m pip install --upgrade pip
8
+ pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu124
9
+ pip install -r requirements.txt
10
+ echo
11
+ echo "Done. Run: ./start_linux.sh (add --nf4 for ~7GB VRAM mode)"
install_windows.bat ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ REM One-time setup: creates a venv and installs dependencies (CUDA 12.4 wheels).
3
+ cd /d %~dp0
4
+ where python >nul 2>nul || (echo Python not found on PATH. Install Python 3.10+ first. & pause & exit /b 1)
5
+ python -m venv venv
6
+ call venv\Scripts\activate
7
+ python -m pip install --upgrade pip
8
+ pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu124
9
+ pip install -r requirements.txt
10
+ echo.
11
+ echo Done. Use start_windows.bat (24GB GPU) or start_windows_lowvram.bat (8GB+ GPU).
12
+ pause
joytag_models.py ADDED
@@ -0,0 +1,1163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # JoyTag model code by fancyfeast (fpgaminer), Apache-2.0.
2
+ # Source: Models.py from https://huggingface.co/spaces/fancyfeast/joytag
3
+ # Project: https://github.com/fpgaminer/joytag
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ import torch
9
+ import torch.backends.cuda
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+ import torchvision
13
+
14
+ from transformers.activations import QuickGELUActivation
15
+ import math
16
+ from einops.layers.torch import Rearrange
17
+ import einops
18
+
19
+
20
+ MODEL_CONFIGS = {
21
+ # Custom models trained from scratch
22
+ # "Standard" definitions:
23
+ # name | layers | width | heads
24
+ # B | 12 | 768 | 12
25
+ # L | 24 | 1024 | 16
26
+ # H | 32 | 1280 | 16
27
+ # G | 48 | 1664 | 16
28
+ # e | 56 | 1792 | 16
29
+ # 22 | 48 | 6144 | 48
30
+
31
+ # B/16, 224, PaLM, GELU
32
+ 'CustomTest6': {
33
+ 'class': 'CLIPLikeModel',
34
+ 'embedding_dim': 768,
35
+ 'num_attention_heads': 12,
36
+ 'activation_cls': nn.GELU,
37
+ 'num_channels': 3,
38
+ 'patch_size': 16,
39
+ 'use_palm_alt': True,
40
+ 'num_layers': 12,
41
+ 'use_mha_alt': False,
42
+ 'good_dropout': False,
43
+ },
44
+
45
+ # GAP head + Sinusoidal positional embeddings + 448 image size
46
+ 'CustomTest18': {
47
+ 'class': 'CLIPLikeModel',
48
+ 'embedding_dim': 768,
49
+ 'num_attention_heads': 12,
50
+ 'activation_cls': nn.GELU,
51
+ 'num_channels': 3,
52
+ 'patch_size': 16,
53
+ 'use_palm_alt': True,
54
+ 'num_layers': 12,
55
+ 'use_mha_alt': False,
56
+ 'good_dropout': False,
57
+ 'use_gap_head': True,
58
+ 'sine_positional_embeddings': True,
59
+ },
60
+
61
+ # SW Model + B/16 + ASL + 448 image size
62
+ # cutout_max_pct = 0
63
+ # mixup_alpha = 0.8
64
+ # noise_level = 2
65
+ # random_resize_method = true
66
+ # total_labels = 6549
67
+ 'SWModel1': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': False},
68
+
69
+ # Sinusoidal positional embeddings
70
+ 'SWModel2': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
71
+
72
+ # Sinusoidal positional embeddings + 224 image size + L/14
73
+ 'SWModel3': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.05, 'layerscale_init': 1e-1, 'use_sine': True},
74
+
75
+ # Sinusoidal positional embeddings + 224 image size + G/14
76
+ 'SWModel4': {'class': 'ViT', 'num_blocks': 48, 'patch_size': 14, 'd_model': 1664, 'mlp_dim': 1664*4, 'num_heads': 16, 'stochdepth_rate': 0.05, 'layerscale_init': 1e-1, 'use_sine': True},
77
+
78
+ # Sinusoidal positional embeddings + focal loss
79
+ 'SWModel5': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
80
+
81
+ 'SWModel6': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
82
+
83
+ 'SWModel7': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
84
+ 'SWModel8': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
85
+ 'SWModel9': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
86
+ 'SWModel10': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
87
+ 'SWModel11': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0, 'use_sine': True},
88
+
89
+ # Trying head_mean_after
90
+ 'SWModel12': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'head_mean_after': True},
91
+
92
+ # Fat boy
93
+ 'SWModel13': {'class': 'ViT', 'num_blocks': 6, 'patch_size': 16, 'd_model': 1536, 'mlp_dim': 1536*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
94
+
95
+ # L/14
96
+ 'SWModel14': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.05, 'layerscale_init': 1e-1, 'use_sine': True},
97
+ 'SWModel15': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.05, 'layerscale_init': 1e-5, 'use_sine': True},
98
+ 'SWModel16': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.10, 'layerscale_init': 1e-1, 'use_sine': True},
99
+ 'SWModel16f': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.10, 'layerscale_init': 1e-1, 'use_sine': True},
100
+ 'SWModel22': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.20, 'layerscale_init': 1e-1, 'use_sine': True},
101
+ 'SWModel25': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 16, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.15, 'layerscale_init': 1e-1, 'use_sine': True, 'cnn_stem': 'conv:c=128;ln;relu;conv:c=256;ln;relu;conv:c=512;ln;relu;conv:c=1024;ln;relu;conv:c=1024,s=1,k=1,p=0'},
102
+
103
+ # CNN stem
104
+ 'SWModel18': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;bn;relu;conv:c=128;bn;relu;conv:c=256;bn;relu;conv:c=512;bn;relu;conv:c=768,s=1,k=1'},
105
+ 'SWModel19': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;bn;relu;conv:c=128;bn;relu;conv:c=128,s=1;bn;relu;conv:c=256;bn;relu;conv:c=256,s=1;bn;relu;conv:c=512;bn;relu;conv:c=768,s=1,k=1,p=0'},
106
+ 'SWModel20': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;ln;relu;conv:c=128;ln;relu;conv:c=256;ln;relu;conv:c=512;ln;relu;conv:c=768,s=1,k=1,p=0'},
107
+ 'SWModel21': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;ln;gelu;conv:c=128;ln;gelu;conv:c=256;ln;gelu;conv:c=512;ln;gelu;conv:c=768,s=1,k=1,p=0'},
108
+ 'SWModel23': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;ln;relu;conv:c=128;ln;relu;conv:c=256;ln;relu;conv:c=512;ln;relu;conv:c=768,s=1,k=1,p=0'},
109
+ 'SWModel24': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;ln;relu;conv:c=128;ln;relu;conv:c=256;ln;relu;conv:c=512;ln;relu;conv:c=768,s=1,k=1,p=0'},
110
+
111
+ # H/14
112
+ 'SWModel17': {'class': 'ViT', 'num_blocks': 32, 'patch_size': 14, 'd_model': 1280, 'mlp_dim': 1280*4, 'num_heads': 16, 'stochdepth_rate': 0.05, 'layerscale_init': 1e-1, 'use_sine': True},
113
+ 'SWModel26': {'class': 'ViT', 'num_blocks': 32, 'patch_size': 14, 'd_model': 1280, 'mlp_dim': 1280*4, 'num_heads': 16, 'stochdepth_rate': 0.15, 'layerscale_init': 1e-1, 'use_sine': True},
114
+ }
115
+
116
+
117
+ class VisionModel(nn.Module):
118
+ image_size: int
119
+ n_tags: int
120
+
121
+ def __init__(self, image_size: int, n_tags: int):
122
+ super().__init__()
123
+
124
+ self.image_size = image_size
125
+ self.n_tags = n_tags
126
+
127
+ @staticmethod
128
+ def load_model(path: Path | str, device: str | None = None) -> 'VisionModel':
129
+ """
130
+ Load a model from a directory.
131
+ :param path: The directory containing the model.
132
+ :return: The model, the image size, and the number of tags.
133
+ """
134
+ with open(Path(path) / 'config.json', 'r') as f:
135
+ config = json.load(f)
136
+
137
+ if (Path(path) / 'model.safetensors').exists():
138
+ from safetensors.torch import load_file
139
+ resume = load_file(Path(path) / 'model.safetensors', device='cpu')
140
+ else:
141
+ resume = torch.load(Path(path) / 'model.pt', map_location=torch.device('cpu'))['model']
142
+
143
+ model_classes = VisionModel.__subclasses__()
144
+ model_cls = next(cls for cls in model_classes if cls.__name__ == config['class'])
145
+
146
+ model = model_cls(**{k: v for k, v in config.items() if k != 'class'})
147
+ model.load(resume)
148
+ if device is not None:
149
+ model = model.to(device)
150
+
151
+ return model
152
+
153
+ @staticmethod
154
+ def from_config(config: dict) -> 'VisionModel':
155
+ model_classes = VisionModel.__subclasses__()
156
+ model_cls = next(cls for cls in model_classes if cls.__name__ == config['class'])
157
+ return model_cls(**{k: v for k, v in config.items() if k != 'class'})
158
+
159
+ def get_optimized_parameters(self, lr: float):
160
+ raise NotImplementedError
161
+
162
+ def save(self):
163
+ raise NotImplementedError
164
+
165
+ def load(self, state_dict):
166
+ raise NotImplementedError
167
+
168
+
169
+ def basic_calculate_loss(preds: dict[str, torch.Tensor], batch: dict, pos_weight: torch.Tensor | None, loss_type: str):
170
+ def asl_helper(preds, target):
171
+ p = F.softmax(preds, dim=1)
172
+ xs_pos = p.clamp(min=1e-6)
173
+ xs_neg = (1 - p).clamp(min=1e-6)
174
+
175
+ los_pos = torch.log(torch.gather(xs_pos, 1, target.unsqueeze(1))).sum()
176
+ los_neg = torch.log(xs_neg)
177
+ los_neg = los_neg.sum() - torch.gather(los_neg, 1, target.unsqueeze(1)).sum()
178
+ loss = los_pos + los_neg
179
+
180
+ return -loss
181
+
182
+ if loss_type == "ce":
183
+ loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'])
184
+ elif loss_type == "weighted":
185
+ loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], pos_weight=pos_weight)
186
+ elif loss_type == "focal":
187
+ gamma = 2
188
+ p = torch.sigmoid(preds['tags'])
189
+ ce_loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], reduction='none')
190
+ p_t = p * batch['tags'] + (1 - p) * (1 - batch['tags'])
191
+ loss = ce_loss * ((1 - p_t) ** gamma)
192
+ loss = loss.mean()
193
+ elif loss_type == "focal2":
194
+ gamma = 2
195
+ p = torch.sigmoid(preds['tags'])
196
+ ce_loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], reduction='none')
197
+ p_t = p * batch['tags'] + (1 - p) * (1 - batch['tags'])
198
+ loss = ce_loss * ((1 - p_t) ** gamma) * 256
199
+ loss = loss.mean()
200
+ elif loss_type == "asl":
201
+ p = torch.sigmoid(preds['tags'])
202
+ xs_pos = p
203
+ xs_neg = 1 - p
204
+
205
+ los_pos = batch['tags'] * torch.log(xs_pos.clamp(min=1e-6))
206
+ los_neg = (1 - batch['tags']) * torch.log(xs_neg.clamp(min=1e-6))
207
+ loss = los_pos + los_neg
208
+ loss = -loss.sum()
209
+
210
+ # Rating
211
+ loss = loss + asl_helper(preds['rating'], batch['rating'])
212
+
213
+ # Score
214
+ loss = loss + asl_helper(preds['score'], batch['score'])
215
+ elif loss_type == "asl2":
216
+ p = torch.sigmoid(preds['tags'])
217
+ xs_pos = p
218
+ xs_neg = 1 - p
219
+
220
+ los_pos = batch['tags'] * torch.log(xs_pos.clamp(min=1e-6))
221
+ los_neg = (1 - batch['tags']) * torch.log(xs_neg.clamp(min=1e-6))
222
+ loss = -los_pos - los_neg
223
+ loss = loss.sum()
224
+ elif loss_type == "asl3":
225
+ p = torch.sigmoid(preds['tags'])
226
+ xs_pos = p
227
+ xs_neg = 1 - p
228
+
229
+ los_pos = batch['tags'] * torch.log(xs_pos.clamp(min=1e-6))
230
+ los_neg = (1 - batch['tags']) * torch.log(xs_neg.clamp(min=1e-6))
231
+ loss = -los_pos - los_neg
232
+ loss = loss.mean()
233
+ elif loss_type == "asl4":
234
+ p = torch.sigmoid(preds['tags'])
235
+ xs_pos = p
236
+ xs_neg = 1 - p
237
+
238
+ los_pos = batch['tags'] * torch.log(xs_pos.clamp(min=1e-6))
239
+ los_neg = (1 - batch['tags']) * torch.log(xs_neg.clamp(min=1e-6))
240
+ loss = -los_pos - los_neg
241
+ loss = loss.mean() * 128
242
+ elif loss_type == "asl5":
243
+ loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], pos_weight=pos_weight) * 128
244
+ elif loss_type == "asl6":
245
+ loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], pos_weight=pos_weight) * 256
246
+ elif loss_type == "asl7":
247
+ loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], pos_weight=pos_weight) * 2
248
+ else:
249
+ raise ValueError(f"Invalid loss type: {loss_type}")
250
+
251
+ return loss
252
+
253
+
254
+ class CLIPMlp(nn.Module):
255
+ def __init__(self, hidden_size: int, intermediate_size: int, activation_cls):
256
+ super().__init__()
257
+ self.activation_fn = activation_cls()
258
+ self.fc1 = nn.Linear(hidden_size, intermediate_size)
259
+ self.fc2 = nn.Linear(intermediate_size, hidden_size)
260
+
261
+ def forward(self, hidden_states: torch.Tensor):
262
+ hidden_states = self.fc1(hidden_states)
263
+ hidden_states = self.activation_fn(hidden_states)
264
+ hidden_states = self.fc2(hidden_states)
265
+ return hidden_states
266
+
267
+
268
+ class FastCLIPAttention2(nn.Module):
269
+ """Fast Attention module for CLIP-like. This is NOT a drop-in replacement for CLIPAttention, since it adds additional flexibility. Mainly uses xformers."""
270
+ def __init__(self, hidden_size: int, out_dim: int, num_attention_heads: int, out_seq_len: Optional[int] = None, norm_qk: bool = False):
271
+ super().__init__()
272
+ self.out_seq_len = out_seq_len
273
+ self.embed_dim = hidden_size
274
+ self.out_dim = out_dim
275
+ self.norm_qk = norm_qk
276
+ self.num_heads = num_attention_heads
277
+ self.head_dim = hidden_size // num_attention_heads
278
+ assert self.head_dim * num_attention_heads == self.embed_dim, "embed_dim must be divisible by num_attention_heads"
279
+
280
+ self.q_proj = nn.Linear(self.embed_dim, self.embed_dim)
281
+ self.kv_proj = nn.Linear(self.embed_dim, self.embed_dim * 2)
282
+ self.out_proj = nn.Linear(self.embed_dim, self.out_dim)
283
+
284
+ if self.norm_qk:
285
+ self.query_norm = nn.LayerNorm(self.embed_dim)
286
+ self.key_norm = nn.LayerNorm(self.embed_dim)
287
+
288
+ #def _shape(self, tensor: torch.Tensor, seq_len: int, bsz: int):
289
+ # return tensor.view(bsz, seq_len, self.num_heads, self.head_dim).contiguous()
290
+
291
+ def forward(self, query_states: torch.Tensor, kv_states: torch.Tensor) -> torch.Tensor:
292
+ bsz, src_len, embed_dim = kv_states.size()
293
+ if self.out_seq_len is not None:
294
+ tgt_len = self.out_seq_len
295
+ else:
296
+ tgt_len = src_len
297
+
298
+ kv_states = self.kv_proj(kv_states) # (bsz, src_len, embed_dim * 2)
299
+ q_states = self.q_proj(query_states[:, :tgt_len]) # (bsz, tgt_len, embed_dim)
300
+
301
+ # NOTE: It is not clear if LayerNorm should be applied to the embed_dim, or to the head_dim
302
+ if self.norm_qk:
303
+ q_states = self.query_norm(q_states).type(q_states.dtype)
304
+ k_states = self.key_norm(kv_states[:, :, :embed_dim]).type(kv_states.dtype)
305
+ v_states = kv_states[:, :, embed_dim:]
306
+ else:
307
+ k_states = kv_states[:, :, :embed_dim]
308
+ v_states = kv_states[:, :, embed_dim:]
309
+
310
+ q_states = q_states.view(bsz, tgt_len, self.num_heads, self.head_dim).transpose(1, 2) # (bsz, num_heads, tgt_len, head_dim)
311
+ k_states = k_states.view(bsz, src_len, self.num_heads, self.head_dim).transpose(1, 2) # (bsz, num_heads, src_len, head_dim)
312
+ v_states = v_states.view(bsz, src_len, self.num_heads, self.head_dim).transpose(1, 2) # (bsz, num_heads, src_len, head_dim)
313
+
314
+ # Performs scale of query_states, attention, and softmax
315
+ with torch.backends.cuda.sdp_kernel(enable_math=False):
316
+ x = F.scaled_dot_product_attention(q_states, k_states, v_states) # (bsz, num_heads, tgt_len, head_dim)
317
+ x = x.transpose(1, 2).contiguous().view(bsz, tgt_len, embed_dim) # (bsz, tgt_len, embed_dim)
318
+
319
+ # Projection
320
+ x = self.out_proj(x) # (bsz, tgt_len, out_dim)
321
+
322
+ return x
323
+
324
+
325
+ class SkipInit(nn.Module):
326
+ def __init__(self, hidden_size: int, channel_wise: bool, init_scale: float):
327
+ super().__init__()
328
+ self.hidden_size = hidden_size
329
+ self.channel_wise = channel_wise
330
+ self.init_scale = init_scale
331
+
332
+ if self.channel_wise:
333
+ self.scale = nn.Parameter(torch.ones(hidden_size) * init_scale)
334
+ else:
335
+ self.scale = nn.Parameter(torch.tensor(init_scale))
336
+
337
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
338
+ return x * self.scale
339
+
340
+
341
+ class FastCLIPEncoderLayer(nn.Module):
342
+ def __init__(
343
+ self,
344
+ hidden_size: int,
345
+ num_attention_heads: int,
346
+ out_seq_len: Optional[int],
347
+ activation_cls = QuickGELUActivation,
348
+ use_palm_alt: bool = False,
349
+ norm_qk: bool = False,
350
+ skip_init: Optional[float] = None,
351
+ stochastic_depth: Optional[float] = None,
352
+ ):
353
+ super().__init__()
354
+
355
+ self.use_palm_alt = use_palm_alt
356
+ self.stochastic_depth = stochastic_depth
357
+
358
+ self.self_attn = FastCLIPAttention2(
359
+ hidden_size=hidden_size,
360
+ out_dim=hidden_size,
361
+ num_attention_heads=num_attention_heads,
362
+ out_seq_len=out_seq_len,
363
+ norm_qk=norm_qk,
364
+ )
365
+ self.mlp = CLIPMlp(hidden_size, 4 * hidden_size, activation_cls)
366
+ self.layer_norm1 = nn.LayerNorm(hidden_size)
367
+ if not use_palm_alt:
368
+ self.layer_norm2 = nn.LayerNorm(hidden_size)
369
+
370
+ if skip_init is not None:
371
+ self.attn_skip_init = SkipInit(hidden_size, channel_wise=True, init_scale=skip_init)
372
+ self.mlp_skip_init = SkipInit(hidden_size, channel_wise=True, init_scale=skip_init)
373
+ else:
374
+ self.attn_skip_init = nn.Identity()
375
+ self.mlp_skip_init = nn.Identity()
376
+
377
+ def forward(self, hidden_states: torch.Tensor):
378
+ residual = hidden_states
379
+ hidden_states = self.layer_norm1(hidden_states)
380
+
381
+ if not self.use_palm_alt:
382
+ hidden_states = self.self_attn(query_states=hidden_states, kv_states=hidden_states)
383
+ hidden_states = self.attn_skip_init(hidden_states)
384
+ hidden_states = hidden_states + residual[:, :hidden_states.size(1)]
385
+
386
+ residual = hidden_states
387
+ hidden_states = self.layer_norm2(hidden_states)
388
+ hidden_states = self.mlp(hidden_states)
389
+ hidden_states = self.mlp_skip_init(hidden_states)
390
+ hidden_states = hidden_states + residual
391
+ else:
392
+ # An alternative implementation inspired by the PALM paper
393
+ # By performing the attention and MLP in parallel it's possible to fuse the linear projections of the attention and MLP layers
394
+ # We don't do that here yet, but that supposedly improves efficiency without hurting performance
395
+ attn = self.self_attn(query_states=hidden_states, kv_states=hidden_states)
396
+ attn = self.attn_skip_init(attn)
397
+ mlp = self.mlp(hidden_states[:, :attn.size(1)])
398
+ mlp = self.mlp_skip_init(mlp)
399
+
400
+ if self.stochastic_depth is not None:
401
+ attn = torchvision.ops.stochastic_depth(attn, self.stochastic_depth, mode='row', training=self.training)
402
+ mlp = torchvision.ops.stochastic_depth(mlp, self.stochastic_depth, mode='row', training=self.training)
403
+
404
+ hidden_states = residual[:, :attn.size(1)] + attn + mlp
405
+
406
+ return hidden_states
407
+
408
+
409
+ def sinusoidal_position_embedding(width: int, height: int, depth: int, dtype, device, temperature = 10000):
410
+ """
411
+ Sinusoidal position embedding. Returns a flat tensor of shape (h * w, d).
412
+ """
413
+ assert depth % 4 == 0, "Embedding dimension must be divisible by 4."
414
+
415
+ y, x = torch.meshgrid(torch.arange(height, device=device), torch.arange(width, device=device), indexing="ij")
416
+ omega = torch.arange(depth // 4, device=device) / (depth // 4 - 1)
417
+ omega = 1. / (temperature ** omega)
418
+
419
+ y = y.flatten()[:, None] * omega[None, :]
420
+ x = x.flatten()[:, None] * omega[None, :]
421
+ embedding = torch.cat([x.sin(), x.cos(), y.sin(), y.cos()], dim=1)
422
+
423
+ return embedding.type(dtype)
424
+
425
+
426
+ class CLIPEmbeddingLayer(nn.Module):
427
+ def __init__(self, hidden_size: int, num_channels: int, image_size: int, patch_size: int, patch_dropout: float = 0.0, good_dropout: bool = False, dpn: bool = False, sine_positional_embeddings: bool = False):
428
+ super().__init__()
429
+
430
+ assert image_size % patch_size == 0, "Image dimensions must be divisible by the patch size."
431
+
432
+ seq_len = (image_size // patch_size) ** 2
433
+ self.patch_dropout = patch_dropout
434
+ self.hidden_size = hidden_size
435
+ self.good_dropout = good_dropout
436
+ self.dpn = dpn
437
+ self.sine_positional_embeddings = sine_positional_embeddings
438
+ self.patch_size = patch_size
439
+
440
+ self.patch_embeddings = nn.Conv2d(
441
+ in_channels=num_channels,
442
+ out_channels=hidden_size,
443
+ kernel_size=patch_size,
444
+ stride=patch_size,
445
+ bias=False,
446
+ )
447
+ if not self.sine_positional_embeddings:
448
+ self.positional_embeddings = nn.Embedding(seq_len, hidden_size)
449
+ self.register_buffer("position_ids", torch.arange(seq_len))
450
+
451
+ if self.dpn:
452
+ self.to_patch_embeddings = nn.Sequential(
453
+ Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=patch_size, p2=patch_size),
454
+ nn.LayerNorm(3 * patch_size * patch_size),
455
+ nn.Linear(3 * patch_size * patch_size, hidden_size),
456
+ nn.LayerNorm(hidden_size),
457
+ )
458
+ else:
459
+ self.to_patch_embeddings = nn.Conv2d(
460
+ in_channels=num_channels,
461
+ out_channels=hidden_size,
462
+ kernel_size=patch_size,
463
+ stride=patch_size,
464
+ bias=False,
465
+ )
466
+
467
+ def forward(self, pixel_values: torch.FloatTensor) -> torch.Tensor:
468
+ B, C, H, W = pixel_values.shape
469
+ assert H % self.patch_size == 0, f"Input image height ({H}) needs to be divisible by the patch size ({self.patch_size})."
470
+ assert W % self.patch_size == 0, f"Input image width ({W}) needs to be divisible by the patch size ({self.patch_size})."
471
+
472
+ if self.dpn:
473
+ patches = self.to_patch_embeddings(pixel_values)
474
+ else:
475
+ patches = self.to_patch_embeddings(pixel_values)
476
+ patches = patches.flatten(2).transpose(1, 2)
477
+
478
+ seq_len = patches.shape[1]
479
+ patch_dropout = int(math.ceil((1.0 - self.patch_dropout) * seq_len))
480
+
481
+ if self.sine_positional_embeddings:
482
+ position_embeddings = sinusoidal_position_embedding(W // self.patch_size, H // self.patch_size, self.hidden_size, pixel_values.dtype, pixel_values.device)
483
+ else:
484
+ position_embeddings = self.positional_embeddings(self.position_ids)
485
+
486
+ if patch_dropout == seq_len or not self.training:
487
+ embeddings = patches + position_embeddings
488
+ elif self.good_dropout:
489
+ # Pick random patches to drop out
490
+ # The "good_dropout" variant uses random permutations for each batch item, but is slightly slower and involves more code
491
+
492
+ # The below method is a nice trick to generate a batch of random permutations.
493
+ # Torch (as of 1.13) doesn't have a built-in function to do this, and a for loop of torch.randperm is slow.
494
+ # Based on some benchmarks I measured the generation of the mask and the fetching to be only 50% slower than the non-"good_dropout" variant.
495
+ # And the time taken here is only a fraction of the time spent performing the embedding convolution.
496
+ # Generate a matrix of random numbers between 0 and 1 of shape (B, seq_len)
497
+ patch_mask = torch.rand(B, seq_len, device=patches.device)
498
+ # For each batch tensor, use argsort to convert the random numbers into a permutation of the patch indices
499
+ patch_mask = torch.argsort(patch_mask, dim=1)
500
+ # Truncate
501
+ patch_mask = patch_mask[:, :patch_dropout]
502
+
503
+ embeddings = patches.gather(1, patch_mask.unsqueeze(-1).expand(-1, -1, self.hidden_size)) + position_embeddings[patch_mask]
504
+ else:
505
+ # The non-"good_dropout" variant uses a single random permutation for all batch items, but is faster and uses less code
506
+ indices = torch.randperm(seq_len, device=pixel_values.device)[:patch_dropout]
507
+ embeddings = patches[:, indices, :] + position_embeddings[indices.expand(1, -1)]
508
+
509
+ return embeddings
510
+
511
+
512
+ class MHAPoolingHead(nn.Module):
513
+ def __init__(self, hidden_size: int, num_attention_heads: int, activation_cls, out_dim: int, alt_style: bool, norm_qk: bool):
514
+ super().__init__()
515
+
516
+ self.out_dim = out_dim if not alt_style else hidden_size
517
+
518
+ self.probe = nn.Parameter(torch.randn(hidden_size))
519
+
520
+ self.mlp = CLIPMlp(hidden_size, 4 * hidden_size, activation_cls)
521
+ self.layer_norm = nn.LayerNorm(hidden_size)
522
+ self.pooling_head = nn.Linear(hidden_size, 1)
523
+
524
+ self.self_attn = FastCLIPAttention2(
525
+ hidden_size=hidden_size,
526
+ out_dim=self.out_dim,
527
+ num_attention_heads=num_attention_heads,
528
+ out_seq_len=1,
529
+ norm_qk=norm_qk,
530
+ )
531
+ self.mlp = CLIPMlp(self.out_dim, 4 * self.out_dim, activation_cls)
532
+ self.layer_norm1 = nn.LayerNorm(hidden_size)
533
+ self.layer_norm2 = nn.LayerNorm(self.out_dim)
534
+
535
+ if alt_style:
536
+ self.final_proj = nn.Linear(hidden_size, out_dim)
537
+ else:
538
+ self.final_proj = nn.Identity()
539
+
540
+ def forward(self, hidden_states: torch.Tensor):
541
+ hidden_states = self.layer_norm1(hidden_states)
542
+ query_states = self.probe.unsqueeze(0).unsqueeze(0).expand(hidden_states.size(0), 1, -1)
543
+
544
+ hidden_states = self.self_attn(query_states=query_states, kv_states=hidden_states)
545
+ # We don't use a residual connection here because the out_dim is different from the hidden_size
546
+
547
+ residual = hidden_states
548
+ hidden_states = self.layer_norm2(hidden_states)
549
+ hidden_states = self.mlp(hidden_states)
550
+ hidden_states = hidden_states + residual
551
+ hidden_states = self.final_proj(hidden_states)
552
+
553
+ return hidden_states.squeeze(1)
554
+
555
+
556
+ class GAPHead(nn.Module):
557
+ def __init__(self, hidden_size: int, out_dim: int):
558
+ super().__init__()
559
+
560
+ self.norm = nn.LayerNorm(hidden_size)
561
+ self.proj = nn.Linear(hidden_size, out_dim)
562
+
563
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
564
+ x = x.mean(dim=1)
565
+ x = self.norm(x)
566
+ x = self.proj(x)
567
+ return x
568
+
569
+
570
+ class CLIPLikeModel(VisionModel):
571
+ def __init__(
572
+ self,
573
+ n_tags: int,
574
+ embedding_dim: int,
575
+ num_attention_heads: int,
576
+ activation_cls,
577
+ num_channels: int,
578
+ image_size: int,
579
+ patch_size: int,
580
+ patch_dropout: float,
581
+ use_palm_alt: bool,
582
+ num_layers: int,
583
+ use_mha_alt: bool,
584
+ loss_type: str,
585
+ good_dropout: bool=False,
586
+ dpn: bool=False,
587
+ sine_positional_embeddings: bool=False,
588
+ norm_qk: bool = False,
589
+ no_wd_bias: bool = False,
590
+ use_gap_head: bool = False,
591
+ skip_init: Optional[float] = None,
592
+ stochastic_depth: Optional[float] = None,
593
+ ):
594
+ super().__init__(image_size, n_tags)
595
+
596
+ out_dim = n_tags
597
+ self.n_tags = n_tags
598
+ self.loss_type = loss_type
599
+ self.no_wd_bias = no_wd_bias
600
+
601
+ stochastic_depth_space = torch.linspace(0, stochastic_depth, num_layers) if stochastic_depth is not None else None
602
+
603
+ self.embedding_layer = CLIPEmbeddingLayer(embedding_dim, num_channels, image_size, patch_size, patch_dropout, good_dropout, dpn, sine_positional_embeddings)
604
+ self.pre_layer_norm = nn.LayerNorm(embedding_dim)
605
+ self.encoder_layers = nn.ModuleList([FastCLIPEncoderLayer(
606
+ hidden_size=embedding_dim,
607
+ num_attention_heads=num_attention_heads,
608
+ out_seq_len=None,
609
+ activation_cls=activation_cls,
610
+ use_palm_alt=use_palm_alt,
611
+ norm_qk=norm_qk,
612
+ skip_init=skip_init,
613
+ stochastic_depth=stochastic_depth_space[i].item() if stochastic_depth_space is not None else None,
614
+ ) for i in range(num_layers)])
615
+
616
+ if use_gap_head:
617
+ self.pooling_head = GAPHead(embedding_dim, out_dim)
618
+ else:
619
+ self.pooling_head = MHAPoolingHead(embedding_dim, num_attention_heads, activation_cls, out_dim, use_mha_alt, norm_qk=norm_qk)
620
+
621
+ def forward(self, batch):
622
+ hidden_states = self.embedding_layer(batch['image'])
623
+ hidden_states = self.pre_layer_norm(hidden_states)
624
+
625
+ for layer in self.encoder_layers:
626
+ hidden_states = layer(hidden_states)
627
+
628
+ preds = self.pooling_head(hidden_states)
629
+
630
+ result = {
631
+ 'tags': preds,
632
+ }
633
+
634
+ return result
635
+
636
+ def calculate_loss(self, preds, batch, pos_weight):
637
+ return basic_calculate_loss(preds, batch, pos_weight, self.loss_type)
638
+
639
+ def get_optimized_parameters(self, lr: float):
640
+ if self.no_wd_bias:
641
+ return self.get_optimized_parameters_no_wd_bias()
642
+ else:
643
+ return self.parameters()
644
+
645
+ def get_optimized_parameters_no_wd_bias(self):
646
+ decay = []
647
+ no_decay = []
648
+
649
+ for name, param in self.named_parameters():
650
+ if not param.requires_grad:
651
+ continue
652
+
653
+ if len(param.shape) == 1 or name.endswith(".bias"):
654
+ no_decay.append(param)
655
+ print(f'No decay: {name}')
656
+ else:
657
+ decay.append(param)
658
+
659
+ return [
660
+ {'params': decay},
661
+ {'params': no_decay, 'weight_decay': 0.},
662
+ ]
663
+
664
+ def save(self):
665
+ return self.state_dict()
666
+
667
+ def load(self, state_dict):
668
+ self.load_state_dict(state_dict)
669
+
670
+
671
+ class MaskedAutoEncoderViT(nn.Module):
672
+ def __init__(
673
+ self,
674
+ n_tags: int,
675
+
676
+ embedding_dim: int,
677
+ num_attention_heads: int,
678
+ activation_cls,
679
+ num_channels: int,
680
+ image_size: int,
681
+ patch_size: int,
682
+ num_layers: int,
683
+ loss_type: str,
684
+ sine_positional_embeddings: bool=False,
685
+
686
+ decoder_embedding_dim: int = 512,
687
+ decoder_num_attention_heads: int = 8,
688
+ decoder_num_layers: int = 6,
689
+ decoder_force_projection: bool = False,
690
+
691
+ masking_ratio: float = 0.75,
692
+ mae_loss_weight: float = 1.0,
693
+ mae_normalize_targets: bool = False,
694
+ mae_post_norm: bool = False,
695
+ ):
696
+ super().__init__()
697
+
698
+ self.n_tags = n_tags
699
+ self.seq_len = (image_size // patch_size) ** 2
700
+ self.embedding_dim = embedding_dim
701
+ self.decoder_embedding_dim = decoder_embedding_dim
702
+ self.sine_positional_embeddings = sine_positional_embeddings
703
+ self.image_size = image_size
704
+ self.patch_size = patch_size
705
+ self.masking_ratio = masking_ratio
706
+ self.loss_type = loss_type
707
+ self.mae_loss_weight = mae_loss_weight
708
+ self.mae_normalize_targets = mae_normalize_targets
709
+
710
+ if not self.sine_positional_embeddings:
711
+ self.positional_embeddings = nn.Embedding(self.seq_len, embedding_dim)
712
+ self.decoder_positional_embeddings = nn.Embedding(self.seq_len, decoder_embedding_dim)
713
+ self.register_buffer("position_ids", torch.arange(self.seq_len))
714
+
715
+ self.to_patches = Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=patch_size, p2=patch_size)
716
+ self.patch_embedder = nn.Linear(num_channels * patch_size * patch_size, embedding_dim)
717
+
718
+ # Encoder
719
+ self.pre_layer_norm = nn.LayerNorm(embedding_dim)
720
+ self.encoder_layers = nn.ModuleList([FastCLIPEncoderLayer(
721
+ hidden_size=embedding_dim,
722
+ num_attention_heads=num_attention_heads,
723
+ out_seq_len=None,
724
+ activation_cls=activation_cls,
725
+ use_palm_alt=True,
726
+ norm_qk=False,
727
+ skip_init=None,
728
+ ) for _ in range(num_layers)])
729
+
730
+ # Head for classification
731
+ self.pooling_head = GAPHead(embedding_dim, n_tags)
732
+
733
+ # Decoder
734
+ if embedding_dim != decoder_embedding_dim or decoder_force_projection:
735
+ self.encoder_to_decoder_proj = nn.Linear(embedding_dim, decoder_embedding_dim)
736
+ else:
737
+ self.encoder_to_decoder_proj = nn.Identity()
738
+ self.decoder_pre_layer_norm = nn.LayerNorm(decoder_embedding_dim)
739
+ self.decoder_layers = nn.ModuleList([FastCLIPEncoderLayer(
740
+ hidden_size=decoder_embedding_dim,
741
+ num_attention_heads=decoder_num_attention_heads,
742
+ out_seq_len=None,
743
+ activation_cls=activation_cls,
744
+ use_palm_alt=True,
745
+ norm_qk=False,
746
+ skip_init=None,
747
+ ) for _ in range(decoder_num_layers)])
748
+
749
+ if mae_post_norm:
750
+ self.decoder_to_pixel_values = nn.Sequential(
751
+ nn.LayerNorm(decoder_embedding_dim),
752
+ nn.Linear(decoder_embedding_dim, num_channels * patch_size * patch_size)
753
+ )
754
+ else:
755
+ self.decoder_to_pixel_values = nn.Linear(decoder_embedding_dim, num_channels * patch_size * patch_size)
756
+ self.mask_token = nn.Parameter(torch.zeros(decoder_embedding_dim))
757
+ torch.nn.init.normal_(self.mask_token, std=0.02)
758
+
759
+ def forward(self, batch):
760
+ pixel_values = batch['image']
761
+ device = pixel_values.device
762
+ B, C, H, W = pixel_values.shape
763
+ assert H % self.patch_size == 0, f"Input image height ({H}) needs to be divisible by the patch size ({self.patch_size})."
764
+ assert W % self.patch_size == 0, f"Input image width ({W}) needs to be divisible by the patch size ({self.patch_size})."
765
+
766
+ # Convert image to patches (B, seq_len, C * patch_size * patch_size)
767
+ patches = self.to_patches(pixel_values)
768
+ seq_len = patches.shape[1]
769
+ num_masked = int(self.masking_ratio * seq_len)
770
+
771
+ # For each batch tensor, use argsort to convert the random numbers into a permutation of the patch indices
772
+ # From this we can get the masked and unmasked indices
773
+ patch_mask = torch.rand(B, seq_len, device=device)
774
+ patch_mask = torch.argsort(patch_mask, dim=1)
775
+ masked_indices, unmasked_indices = patch_mask[:, :num_masked], patch_mask[:, num_masked:]
776
+ batch_range = torch.arange(B, device=device)[:, None]
777
+
778
+ # Masked and unmasked patches
779
+ unmasked_patches = patches[batch_range, unmasked_indices]
780
+ masked_patches = patches[batch_range, masked_indices]
781
+
782
+ # Embed unmasked patches for the encoder (B, seq_len, embedding_dim)
783
+ tokens = self.patch_embedder(unmasked_patches)
784
+
785
+ if self.sine_positional_embeddings:
786
+ position_embeddings = sinusoidal_position_embedding(W // self.patch_size, H // self.patch_size, self.embedding_dim, pixel_values.dtype, device)
787
+ decoder_position_embeddings = sinusoidal_position_embedding(W // self.patch_size, H // self.patch_size, self.decoder_embedding_dim, pixel_values.dtype, device)
788
+ else:
789
+ position_embeddings = self.positional_embeddings(self.position_ids)
790
+ decoder_position_embeddings = self.decoder_positional_embeddings(self.position_ids)
791
+
792
+ # Add position embeddings
793
+ tokens = tokens + position_embeddings[unmasked_indices]
794
+
795
+ # Run the encoder
796
+ encoded_tokens = self.pre_layer_norm(tokens)
797
+
798
+ for layer in self.encoder_layers:
799
+ encoded_tokens = layer(encoded_tokens)
800
+
801
+ # Label predictions
802
+ if self.training:
803
+ preds = self.pooling_head(encoded_tokens)
804
+ else:
805
+ # During inference, classify using the entire image
806
+ # But we'll do the usual for the MAE part, just so we can see how MAE is performing during validation
807
+ tokens = self.patch_embedder(patches)
808
+ tokens = tokens + position_embeddings
809
+ tokens = self.pre_layer_norm(tokens)
810
+ for layer in self.encoder_layers:
811
+ tokens = layer(tokens)
812
+ preds = self.pooling_head(tokens)
813
+
814
+ # Projection for the decoder and position embeddings
815
+ decoder_tokens = self.encoder_to_decoder_proj(encoded_tokens)
816
+ decoder_tokens = decoder_tokens + decoder_position_embeddings[unmasked_indices]
817
+
818
+ # Fill in the masked patches
819
+ mask_tokens = einops.repeat(self.mask_token, 'd -> b n d', b = B, n = num_masked)
820
+ mask_tokens = mask_tokens + decoder_position_embeddings[masked_indices]
821
+ decoder_tokens = torch.cat([decoder_tokens, mask_tokens], dim=1)
822
+
823
+ # Run the decoder
824
+ decoded_tokens = self.decoder_pre_layer_norm(decoder_tokens)
825
+
826
+ for layer in self.decoder_layers:
827
+ decoded_tokens = layer(decoded_tokens)
828
+
829
+ # Only predict the masked patches
830
+ # All the masked patches are at the end of the sequence
831
+ decoded_tokens = decoded_tokens[:, -num_masked:]
832
+ pred_pixel_values = self.decoder_to_pixel_values(decoded_tokens)
833
+
834
+ # Calculate the mae loss
835
+ if self.mae_normalize_targets:
836
+ # Normalize each patch by its mean and variance. The ViCHA paper says this provides better results
837
+ means = masked_patches.mean(dim=-1, keepdim=True)
838
+ vars = masked_patches.var(dim=-1, keepdim=True)
839
+ target = (masked_patches - means) / (vars + 1e-6)**0.5
840
+ mae_loss = F.mse_loss(pred_pixel_values, target)
841
+ else:
842
+ mae_loss = F.mse_loss(pred_pixel_values, masked_patches)
843
+ mae_loss = mae_loss * self.mae_loss_weight
844
+
845
+ return {
846
+ 'tags': preds,
847
+ 'mae_loss': mae_loss,
848
+ }
849
+
850
+ def calculate_loss(self, preds, batch, pos_weight):
851
+ return basic_calculate_loss(preds, batch, pos_weight, self.loss_type) + preds['mae_loss']
852
+
853
+ def get_optimized_parameters(self, lr: float):
854
+ return self.parameters()
855
+
856
+ def save(self):
857
+ return self.state_dict()
858
+
859
+ def load(self, state_dict):
860
+ self.load_state_dict(state_dict)
861
+
862
+
863
+ class StochDepth(nn.Module):
864
+ def __init__(self, drop_rate: float, scale_by_keep: bool = False):
865
+ super().__init__()
866
+ self.drop_rate = drop_rate
867
+ self.scale_by_keep = scale_by_keep
868
+
869
+ def forward(self, x):
870
+ if not self.training:
871
+ return x
872
+
873
+ batch_size = x.shape[0]
874
+ r = torch.rand((batch_size, 1, 1), device=x.device)
875
+ keep_prob = 1 - self.drop_rate
876
+ binary_tensor = torch.floor(keep_prob + r)
877
+ if self.scale_by_keep:
878
+ x = x / keep_prob
879
+
880
+ return x * binary_tensor
881
+
882
+
883
+ class SkipInitChannelwise(nn.Module):
884
+ def __init__(self, channels, init_val=1e-6):
885
+ super().__init__()
886
+ self.channels = channels
887
+ self.init_val = init_val
888
+ self.skip = nn.Parameter(torch.ones(channels) * init_val)
889
+
890
+ def forward(self, x):
891
+ return x * self.skip
892
+
893
+
894
+ class PosEmbedding(nn.Module):
895
+ def __init__(self, d_model: int, max_len: int, use_sine: bool, patch_size: int):
896
+ super().__init__()
897
+ self.d_model = d_model
898
+ self.max_len = max_len
899
+ self.use_sine = use_sine
900
+ self.patch_size = patch_size
901
+
902
+ if not self.use_sine:
903
+ self.embedding = nn.Embedding(max_len, d_model)
904
+ nn.init.trunc_normal_(self.embedding.weight, std=0.02)
905
+ self.register_buffer("position_ids", torch.arange(max_len))
906
+
907
+ def forward(self, x, width: int, height: int):
908
+ if self.use_sine:
909
+ position_embeddings = sinusoidal_position_embedding(width // self.patch_size, height // self.patch_size, self.d_model, x.dtype, x.device)
910
+ else:
911
+ position_embeddings = self.embedding(self.position_ids)
912
+
913
+ return x + position_embeddings
914
+
915
+
916
+ class MLPBlock(nn.Module):
917
+ def __init__(self, d_model: int, d_ff: int, stochdepth_rate: float):
918
+ super().__init__()
919
+ self.linear1 = nn.Linear(d_model, d_ff)
920
+ self.linear2 = nn.Linear(d_ff, d_model)
921
+ self.activation = nn.GELU()
922
+ if stochdepth_rate > 0:
923
+ self.stochdepth = StochDepth(stochdepth_rate, scale_by_keep=True)
924
+ else:
925
+ self.stochdepth = None
926
+
927
+ def forward(self, x):
928
+ x = self.linear1(x)
929
+ x = self.activation(x)
930
+ if self.stochdepth is not None:
931
+ x = self.stochdepth(x)
932
+ x = self.linear2(x)
933
+ return x
934
+
935
+
936
+ class ViTBlock(nn.Module):
937
+ def __init__(self, num_heads: int, d_model: int, d_ff: int, layerscale_init: float, stochdepth_rate: float):
938
+ super().__init__()
939
+ self.num_heads = num_heads
940
+ self.d_model = d_model
941
+
942
+ assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
943
+
944
+ # MHA
945
+ self.norm1 = nn.LayerNorm(d_model)
946
+ self.qkv_proj = nn.Linear(d_model, d_model * 3)
947
+ self.out_proj = nn.Linear(d_model, d_model)
948
+ self.skip_init1 = SkipInitChannelwise(channels=d_model, init_val=layerscale_init)
949
+ self.stochdepth1 = StochDepth(stochdepth_rate, scale_by_keep=True) if stochdepth_rate > 0 else None
950
+
951
+ # MLP
952
+ self.norm2 = nn.LayerNorm(d_model)
953
+ self.mlp = MLPBlock(d_model, d_ff, stochdepth_rate)
954
+ self.skip_init2 = SkipInitChannelwise(channels=d_model, init_val=layerscale_init)
955
+ self.stochdepth2 = StochDepth(stochdepth_rate, scale_by_keep=True) if stochdepth_rate > 0 else None
956
+
957
+ def forward(self, x):
958
+ bsz, src_len, embed_dim = x.shape
959
+
960
+ out = x
961
+ out = self.norm1(out)
962
+
963
+ # MHA
964
+ qkv_states = self.qkv_proj(out).split(self.d_model, dim=-1)
965
+ q_states = qkv_states[0].view(bsz, src_len, self.num_heads, embed_dim // self.num_heads).transpose(1, 2) # (bsz, num_heads, src_len, embed_dim // num_heads)
966
+ k_states = qkv_states[1].view(bsz, src_len, self.num_heads, embed_dim // self.num_heads).transpose(1, 2) # (bsz, num_heads, src_len, embed_dim // num_heads)
967
+ v_states = qkv_states[2].view(bsz, src_len, self.num_heads, embed_dim // self.num_heads).transpose(1, 2) # (bsz, num_heads, src_len, embed_dim // num_heads)
968
+
969
+ with torch.backends.cuda.sdp_kernel(enable_math=False):
970
+ out = F.scaled_dot_product_attention(q_states, k_states, v_states) # (bsz, num_heads, tgt_len, head_dim)
971
+ out = out.transpose(1, 2).contiguous().view(bsz, src_len, embed_dim) # (bsz, tgt_len, embed_dim)
972
+
973
+ out = self.out_proj(out)
974
+
975
+ out = self.skip_init1(out)
976
+ if self.stochdepth1 is not None:
977
+ out = self.stochdepth1(out)
978
+ x = out + x
979
+
980
+ out = self.norm2(x)
981
+ out = self.mlp(out)
982
+ out = self.skip_init2(out)
983
+ if self.stochdepth2 is not None:
984
+ out = self.stochdepth2(out)
985
+
986
+ out = out + x
987
+
988
+ return out
989
+
990
+
991
+ def CaiT_LayerScale_init(network_depth):
992
+ if network_depth <= 18:
993
+ return 1e-1
994
+ elif network_depth <= 24:
995
+ return 1e-5
996
+ else:
997
+ return 1e-6
998
+
999
+
1000
+ class CNNLayerNorm(nn.Module):
1001
+ def __init__(self, d_model: int):
1002
+ super().__init__()
1003
+ self.norm = nn.LayerNorm(d_model)
1004
+
1005
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1006
+ x = x.transpose(1, 3)
1007
+ x = self.norm(x)
1008
+ x = x.transpose(1, 3)
1009
+ return x
1010
+
1011
+
1012
+ class CNNStem(nn.Module):
1013
+ def __init__(self, config: str):
1014
+ super().__init__()
1015
+ self.config = config
1016
+
1017
+ layers = []
1018
+ channels = 3
1019
+
1020
+ for line in config.split(";"):
1021
+ ty, line = line.split(":") if ":" in line else (line, "")
1022
+ options = line.split(",")
1023
+ options = [o.split("=") for o in options] if line else []
1024
+ options = {k: v for k, v in options}
1025
+
1026
+ if ty == 'conv':
1027
+ layers.append(nn.Conv2d(
1028
+ in_channels=channels,
1029
+ out_channels=int(options['c']),
1030
+ kernel_size=int(options['k'] if 'k' in options else 3),
1031
+ stride=int(options['s'] if 's' in options else 2),
1032
+ bias=True,
1033
+ padding=int(options['p'] if 'p' in options else 1),
1034
+ ))
1035
+ channels = int(options['c'])
1036
+ elif ty == 'bn':
1037
+ layers.append(nn.BatchNorm2d(channels))
1038
+ elif ty == 'ln':
1039
+ layers.append(CNNLayerNorm(channels))
1040
+ elif ty == 'relu':
1041
+ layers.append(nn.ReLU())
1042
+ elif ty == 'gelu':
1043
+ layers.append(nn.GELU())
1044
+
1045
+ self.conv = nn.Sequential(*layers)
1046
+
1047
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1048
+ return self.conv(x)
1049
+
1050
+
1051
+ class ViT(VisionModel):
1052
+ def __init__(self,
1053
+ n_tags: int,
1054
+ image_size: int,
1055
+ num_blocks: int,
1056
+ patch_size: int,
1057
+ d_model: int,
1058
+ mlp_dim: int,
1059
+ num_heads: int,
1060
+ stochdepth_rate: float,
1061
+ use_sine: bool,
1062
+ loss_type: str,
1063
+ layerscale_init: Optional[float] = None,
1064
+ head_mean_after: bool = False,
1065
+ cnn_stem: str | None = None,
1066
+ patch_dropout: float = 0.0,
1067
+ ):
1068
+ super().__init__(image_size, n_tags)
1069
+
1070
+ #assert image_size % patch_size == 0, "image_size must be divisible by patch_size"
1071
+ assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
1072
+
1073
+ out_dim = n_tags
1074
+ self.n_tags = n_tags
1075
+ self.loss_type = loss_type
1076
+ self.patch_size = patch_size
1077
+ self.head_mean_after = head_mean_after
1078
+ self.patch_dropout = patch_dropout
1079
+
1080
+ layerscale_init = CaiT_LayerScale_init(num_blocks) if layerscale_init is None else layerscale_init
1081
+ self.patch_embeddings = nn.Conv2d(
1082
+ in_channels=3,
1083
+ out_channels=d_model,
1084
+ kernel_size=patch_size,
1085
+ stride=patch_size,
1086
+ bias=True,
1087
+ ) if cnn_stem is None else CNNStem(cnn_stem)
1088
+ self.pos_embedding = PosEmbedding(d_model, (image_size // patch_size) ** 2, use_sine=use_sine, patch_size=patch_size)
1089
+
1090
+ self.blocks = nn.ModuleList([
1091
+ ViTBlock(num_heads, d_model, mlp_dim, layerscale_init, stochdepth_rate)
1092
+ for _ in range(num_blocks)
1093
+ ])
1094
+
1095
+ self.norm = nn.LayerNorm(d_model)
1096
+ self.head = nn.Linear(d_model, out_dim)
1097
+
1098
+ def forward(self, batch, return_embeddings=False, return_loss: bool = False, pos_weight = None):
1099
+ B, C, H, W = batch['image'].shape
1100
+ assert H % self.patch_size == 0, f"Input image height ({H}) needs to be divisible by the patch size ({self.patch_size})."
1101
+ assert W % self.patch_size == 0, f"Input image width ({W}) needs to be divisible by the patch size ({self.patch_size})."
1102
+
1103
+ x = self.patch_embeddings(batch['image']) # (bsz, d_model, patch_num, patch_num)
1104
+ x = x.flatten(2).transpose(1, 2) # (bsz, patch_num ** 2, d_model)
1105
+ x = self.pos_embedding(x, W, H) # (bsz, patch_num ** 2, d_model)
1106
+
1107
+ # Patch dropout
1108
+ seq_len = x.shape[1]
1109
+ patch_dropout = int(math.ceil((1.0 - self.patch_dropout) * seq_len))
1110
+
1111
+ if patch_dropout != seq_len:
1112
+ # Generate a matrix of random numbers between 0 and 1 of shape (B, seq_len)
1113
+ patch_mask = torch.rand(B, seq_len, device=x.device)
1114
+ # For each batch tensor, use argsort to convert the random numbers into a permutation of the patch indices
1115
+ patch_mask = torch.argsort(patch_mask, dim=1)
1116
+ # Truncate
1117
+ patch_mask = patch_mask[:, :patch_dropout]
1118
+
1119
+ x = x.gather(1, patch_mask.unsqueeze(-1).expand(-1, -1, x.shape[-1]))
1120
+
1121
+ #indices = torch.randperm(seq_len, device=x.device)[:patch_dropout]
1122
+ #x = x[:, indices, :]
1123
+
1124
+ # Transformer
1125
+ for block in self.blocks:
1126
+ x = block(x)
1127
+
1128
+ # Head
1129
+ result = {}
1130
+
1131
+ x = self.norm(x)
1132
+ if self.head_mean_after:
1133
+ x = self.head(x)
1134
+ x = x.mean(dim=1)
1135
+ else:
1136
+ x = x.mean(dim=1)
1137
+ if return_embeddings:
1138
+ result['embeddings'] = x
1139
+ x = self.head(x)
1140
+
1141
+ result['tags'] = x
1142
+
1143
+ if return_loss:
1144
+ result['loss'] = self.calculate_loss(result, batch, pos_weight)
1145
+
1146
+ return result
1147
+
1148
+ def calculate_loss(self, preds, batch, pos_weight):
1149
+ return basic_calculate_loss(preds, batch, pos_weight, self.loss_type)
1150
+
1151
+ def get_optimized_parameters(self, lr: float):
1152
+ return self.parameters()
1153
+
1154
+ def save(self):
1155
+ return self.state_dict()
1156
+
1157
+ def load(self, state_dict):
1158
+ if 'head.weight' in state_dict and 'head.bias' in state_dict and state_dict['head.weight'].shape[0] == (self.n_tags + 9):
1159
+ # Support old models which included 3 rating and 6 score dimensions
1160
+ state_dict['head.weight'] = state_dict['head.weight'][:self.n_tags]
1161
+ state_dict['head.bias'] = state_dict['head.bias'][:self.n_tags]
1162
+
1163
+ self.load_state_dict(state_dict)
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # IMPORTANT: gradio is pinned because 6.14.0 has a rendering regression that hides
2
+ # the Caption Type / Caption Length dropdowns and breaks prompt updates. Do NOT upgrade.
3
+ gradio==6.12.0
4
+ transformers==4.46.3
5
+ accelerate
6
+ torch==2.6.0
7
+ torchvision==0.21.0
8
+ bitsandbytes==0.49.2; sys_platform != "darwin"
9
+ pillow
10
+ einops # required by joytag_models.py (JoyTag tab)
11
+ huggingface_hub
start_linux.sh ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ cd "$(dirname "$0")"
3
+ source venv/bin/activate
4
+ python app.py "$@"
start_windows.bat ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ REM Full-quality bf16 mode (~18GB VRAM)
3
+ cd /d %~dp0
4
+ call venv\Scripts\activate
5
+ set HF_HUB_DISABLE_SYMLINKS_WARNING=1
6
+ python app.py %*
7
+ pause
start_windows_lowvram.bat ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ REM 4-bit NF4 mode (~7GB VRAM) - use this on smaller GPUs or when ComfyUI is also running
3
+ cd /d %~dp0
4
+ call venv\Scripts\activate
5
+ set HF_HUB_DISABLE_SYMLINKS_WARNING=1
6
+ python app.py --nf4 %*
7
+ pause