-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathgenerate.py
More file actions
3804 lines (3577 loc) · 242 KB
/
Copy pathgenerate.py
File metadata and controls
3804 lines (3577 loc) · 242 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import gc
import os
# before transformers/diffusers are imported
os.environ["TRANSFORMERS_VERBOSITY"] = "error" #if not verbose else "info"
os.environ["DIFFUSERS_VERBOSITY"] = "error" #if not verbose else "info"
import base64
import math
import random
from typing import Tuple
import torch
# from torch import autocast
from transformers import models as transformers_models
from diffusers import models as diffusers_models
from transformers import CLIPTextModel, CLIPTokenizer, AutoFeatureExtractor, CLIPSegProcessor, CLIPSegForImageSegmentation
from diffusers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler, IPNDMScheduler, EulerAncestralDiscreteScheduler, EulerDiscreteScheduler, DPMSolverMultistepScheduler, DPMSolverSinglestepScheduler, KDPM2DiscreteScheduler, KDPM2AncestralDiscreteScheduler, HeunDiscreteScheduler, DEISMultistepScheduler, UniPCMultistepScheduler, EDMDPMSolverMultistepScheduler, EDMEulerScheduler, SASolverScheduler, TCDScheduler
from diffusers import AutoencoderKL, UNet2DConditionModel, ControlNetModel
from diffusers.utils import is_accelerate_available
import argparse
from datetime import datetime
from PIL.PngImagePlugin import PngInfo
import codecs
import cv2
import numpy as np
from traceback import print_exc
from PIL import Image, ImageOps, ImageEnhance, ImageColor
from tqdm.auto import tqdm
from time import time
from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker
import inspect
from io import BytesIO
from pathlib import Path
import re
from tokens import get_huggingface_token
import uuid
from dataclasses import dataclass
from json import loads as load_json_str
from importlib import util as import_util
from functools import lru_cache
# for automated downloads via huggingface hub
model_id = "CompVis/stable-diffusion-v1-4"
# for manual model installs
models_local_dir = "models/playgroundv2_aesthetic"
# for metadata, written during model load
using_local_unet, using_local_vae = False,False
# location of textual inversion concepts (if present). (recursively) search for any .bin files. see see: https://huggingface.co/sd-concepts-library
concepts_dir = "models/concepts"
_DEFAULT_DEVICE = "cuda" if torch.cuda.is_available() else "dml" if (import_util.find_spec("torch_directml") is not None) else "cpu"
# one of cpu, cuda, ipu, xpu, mkldnn, opengl, opencl, ideep, hip, ve, ort, mps, xla, lazy, vulkan, meta, hpu
# devices will be set by argparser if used.
DIFFUSION_DEVICE = _DEFAULT_DEVICE
IO_DEVICE = _DEFAULT_DEVICE
# execution device when offloading enabled. Required as a global for now.
OFFLOAD_EXEC_DEVICE = _DEFAULT_DEVICE
# display current image in img2img cycle mode via cv2.
IMAGE_CYCLE_DISPLAY_CV2 = True
# display preprocessed image via cv2 when utilising controlnet preprocessing
PREPROCESS_DISPLAY_CV2 = True
# Window title for cv2 display
CV2_TITLE="Output"
# Window title for cv2 display when displaying preprocessing image
PREPROCESS_CV2_TITLE="ControlNet Input"
# global flag signifying if a v_prediction model is in use.
V_PREDICTION_MODEL = False
# will be read and overridden from the text encoder layer count on model load. Inclusive range limit. 12 for ViT-L/14 (SD1.x) or 23 for ViT-H (SD2.x)
MAX_CLIP_SKIP = 12
# when mixing prompts via concatenation, this will drop embeddings with a weight of zero, instead of adding them as "empty" (zeroed) tensors. Makes a (slight) difference when interpolating prompts, especially for longer sequences.
CONCAT_DROP_ZERO_WEIGHT_EMBEDDINGS = True
# when interpolating prompts, skew ramp-up/ramp-down of prompts, so that both values cross over at the set point. Increasing from 0.5 should reduce lack of definition at the crossover point in concat mode (dynamic length), but will compress or reduce variety in-between prompts.
# should be between 0 and 1, with values >=0.5 being recommended. 0.5 corresponds to no skew.
PROMPT_INTERPOLATION_CROSSOVER_POINT = 0.5
# pixels of separation between individual images in an image grid.
GRID_IMAGE_SEPARATION = 1
# background color of grid images. Accepts any color definition understood by 'PIL.ImageColor', including hex (#rrggbb) and common HTML color names ("darkslategray"). Will be visible both between images (separation) and in empty grid spots (e.g. 8 images on a 3x3 grid)
GRID_IMAGE_BACKGROUND_COLOR = "#2A2B2E"
assert ImageColor.getrgb(GRID_IMAGE_BACKGROUND_COLOR) # check requested color for validity before running the script.
# the non-refined rd64 model variant produces less precise results, but this may yield slightly more reliable behavior with the current thresholding method
SAFETY_PROCESSOR_CLIPSEG_MODEL = "CIDAS/clipseg-rd64-refined" # "CIDAS/clipseg-rd64" # "CIDAS/clipseg-rd16"
# debug option, displays blur heatmaps via cv2 when the safety processor applies masked blur.
SAFETY_PROCESSOR_DISPLAY_MASK_CV2 = False
# increase to boost safety checker thresholds with an extra margin.
SAFETY_LEVEL_GAIN = 1.0
# When using (differential) progress animation, boost the variance of earlier steps to be in-line with expectations. Early animation steps will look more oversaturated and "noisy" instead of undersaturated and "diffuse"
ANIMATE_BOOST_VARIANCE = False
# Compression factor of the sigmoid function when using threshold schedules (gs/controlnet/lora: th2, th5, th7). Values between 20 and 100 recommended. Lower values smooth out the transition, while higher values make the threshold more direct.
SIGMOID_GS_MULT_COMPRESSION = 50
# SDXL pooled extra_embeds always use a CLIP skip of 0. Setting this to False would apply per-token skip values of the standard prompt_embeds before pooling
SDXL_NO_TOKEN_SKIP_ON_POOLED = True
# Always run VAE in tiled and sliced mode. May help performance when overflowing into shared GPU memory instead of producing an OOM error
ALWAYS_MINIMIZE_VAE_MEMORY = False
# For prompts that go beyond the encoder token limit, use the previous chunked processing method instead of moving window encoding.
USE_CHUNKED_LONG_PROMPTS = False
# sigma value of the gaussian distribution used to mix embedding vectors of a token when using window encoding. The higher the value, the less the weighting prioritises the instances closest to the window center.
WINDOW_ENCODING_REDUCE_SIGMA = 0.0
# when using window encoding, after mixing embedding vectors of a token, correct norm to the mean norm of all source vectors (counteract potential vector magnitude shrinking due to mixing)
WINDOW_ENCODING_CORRECT_MAGNITUDE = True
# when processing very long prompts in window encoding mode, the batch may become very large (one batch item per token over 77). this may significantly boost speed (8-10x observed in preliminary testing, likely depends on hardware.).
MODEL_OFFLOAD_TEXT_ENCODERS = True
# Switch to using xformers attention if installed. Can be set to False to manually disable xformers attention regardless of availability.
XFORMERS_ENABLED = True
try:
import xformers
except ImportError:
XFORMERS_ENABLED = False
OUTPUT_DIR_BASE = "outputs"
OUTPUTS_DIR = f"{OUTPUT_DIR_BASE}/generated"
ANIMATIONS_DIR = f"{OUTPUT_DIR_BASE}/animate"
INDIVIDUAL_OUTPUTS_DIR = os.path.join(OUTPUTS_DIR, "individual")
UNPUB_DIR = os.path.join(OUTPUTS_DIR, "unpub")
os.makedirs(OUTPUTS_DIR, exist_ok=True)
os.makedirs(INDIVIDUAL_OUTPUTS_DIR, exist_ok=True)
os.makedirs(UNPUB_DIR, exist_ok=True)
os.makedirs(ANIMATIONS_DIR, exist_ok=True)
SCHEDULER_OPTIONS = {
"ddim": DDIMScheduler,
"lms":LMSDiscreteScheduler,"pndm":PNDMScheduler,
"euler": EulerDiscreteScheduler, "euler_ancestral": EulerAncestralDiscreteScheduler,
"heun": HeunDiscreteScheduler,
"mdpms": DPMSolverMultistepScheduler, "sdpms": DPMSolverSinglestepScheduler,
"kdpm2": KDPM2DiscreteScheduler, "kdpm2_ancestral": KDPM2AncestralDiscreteScheduler,
"deis": DEISMultistepScheduler,
"unipc": UniPCMultistepScheduler,
"tcd": TCDScheduler,
}
EDM_SCHEDULERS = {"mdpms": EDMDPMSolverMultistepScheduler, "euler": EDMEulerScheduler}
IMPLEMENTED_SCHEDULERS = list(SCHEDULER_OPTIONS.keys())
V_PREDICTION_SCHEDULERS = IMPLEMENTED_SCHEDULERS # ["euler", "ddim", "mdpms", "euler_ancestral", "pndm", "lms", "sdpms"]
IMPLEMENTED_GS_SCHEDULES = [None, "sin", "cos", "isin", "icos", "fsin", "anneal5", "ianneal5", "rand", "frand", "buzz", "th2", "th5", "th7", "ith2", "ith5", "ith7"]
# preprocessors for controlnet input images
control_processor_detector_map = {}
try:
import controlnet_aux as ca
control_processor_detector_map = {
"pose":ca.OpenposeDetector, "mlsd":ca.MLSDdetector, "hed":ca.HEDdetector,
"midas":ca.MidasDetector, "zoe":ca.ZoeDetector, "leres":ca.LeresDetector, "normalbae":ca.NormalBaeDetector,
"pidi":ca.PidiNetDetector, "lineart":ca.LineartDetector, "lineart_anime":ca.LineartAnimeDetector,
"shuffle":ca.ContentShuffleDetector,
}
except ImportError:
print(f"unable to import detector(s) for from 'controlnet_aux'. Please install the latest version via 'pip install controlnet_aux --upgrade'")
IMPLEMENTED_CONTROLNET_PREPROCESSORS = ["canny", "blur"] + list(control_processor_detector_map.keys())
# short names for specifying controlnet models. will translate requests for name into requests for CONTROLNET_SHORTNAMES[name]
# Controlnet V1 SD1.x
CONTROLNET_SHORTNAMES = {name:f"lllyasviel/sd-controlnet-{name}" for name in ["canny","depth","hed","mlsd","normal","openpose","scribble","seg"]}
# SD2.1
CONTROLNET_SHORTNAMES |= {name:f"thibaud/controlnet-{name}-diffusers" for name in [f"sd21-{x}" for x in ["canny","depth","hed","openpose","scribble","zoedepth","color"]]}
# Controlnet V1.1 SD1.x 'production ready' - will replace V1 controlnet shortcuts of the same name
CONTROLNET_SHORTNAMES |= {name:f"lllyasviel/control_v11p_sd15_{name}" for name in ["canny", "mlsd", "normalbae", "seg", "inpaint", "lineart", "openpose", "scribble", "softedge"]}
# Controlnet V1.1 SD1.x 'experimental' or with alternate prefix
CONTROLNET_SHORTNAMES |= {"tile":"lllyasviel/control_v11f1e_sd15_tile", "depth":"lllyasviel/control_v11f1p_sd15_depth", "instruct":"lllyasviel/control_v11e_sd15_ip2p", "shuffle":"lllyasviel/control_v11e_sd15_shuffle", "lineart_anime":"lllyasviel/control_v11p_sd15s2_lineart_anime"}
# SDXL controlnets (diffusers)
CONTROLNET_SHORTNAMES |= {
"xl_canny":"diffusers/controlnet-canny-sdxl-1.0", "xl_depth":"diffusers/controlnet-depth-sdxl-1.0", "xl_zoedepth":"diffusers/controlnet-zoe-depth-sdxl-1.0",
"xl_canny_mid":"diffusers/controlnet-canny-sdxl-1.0-mid", "xl_depth_mid":"diffusers/controlnet-depth-sdxl-1.0-mid",
"xl_canny_small":"diffusers/controlnet-canny-sdxl-1.0-small", "xl_depth_small":"diffusers/controlnet-depth-sdxl-1.0-small",
}
# SDXL controlnets - NOTE: the listed sdxl inpaint model requires areas to be inpainted to be blank white instead of blank black
CONTROLNET_SHORTNAMES |= {"xl_openpose":"thibaud/controlnet-openpose-sdxl-1.0", "xl_softedge":"SargeZT/controlnet-sd-xl-1.0-softedge-dexined", "xl_inpaint":"destitech/controlnet-inpaint-dreamer-sdxl|v2"}
CONTROLNET_SHORTNAMES |= {
"xl_union":"xinsir/controlnet-union-sdxl-1.0", "xl_union_promax":"xinsir/controlnet-union-sdxl-1.0|promax|pr/23",
"xl_openpose_2":"xinsir/controlnet-openpose-sdxl-1.0",
"xl_tile":"xinsir/controlnet-tile-sdxl-1.0",
"xl_depth_2":"xinsir/controlnet-depth-sdxl-1.0",
"xl_canny_2":"xinsir/controlnet-canny-sdxl-1.0", "xl_lines":"xinsir/controlnet-scribble-sdxl-1.0", "xl_scribble_anime":"xinsir/anime-painter",
}
# Other Controlnets
CONTROLNET_SHORTNAMES |= {"qrcode": "monster-labs/control_v1p_sd15_qrcode_monster|v2", "xl_qrcode": "monster-labs/control_v1p_sdxl_qrcode_monster"}
# sd2.0 default negative prompt
DEFAULT_NEGATIVE_PROMPT = "" # e.g. dreambot SD2.0 default : "ugly, tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, extra limbs, disfigured, deformed, body out of frame, blurry, bad anatomy, blurred, watermark, grainy, signature, cut off, draft"
# THIS WILL AUTOMATICALLY RESET. DO NOT ADD CUSTOM VALUES HERE. substitutions/shortcuts in prompts. Will be filled with substitions for multi-token custom embeddings.
PROMPT_SUBST = {}
# Custom combinations of "shortname":"substitution string", could be added here.
CUSTOM_PROMPT_SUBST = {
"<negative>":"ugly, tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, extra limbs, disfigured, deformed, body out of frame, blurry, bad anatomy, blurred, watermark, grainy, signature, cut off, draft",
}
# Reference parameters: https://github.com/ChenyangSi/FreeU?tab=readme-ov-file#parameters - may need adjustment depending on custom models/usage/etc.
# Authors recommend: b1 in [1.0, 1.2] (not reflective of their reference values?), b2 in [1.2, 1.6], s1 <= 1.0, s2 <= 1.0
FREEU_DEFAULT_PARAMS = {
"SD1": {"b1": 1.5, "b2": 1.6, "s1": 0.9, "s2": 0.2}, # SD1.5 reference value
"SD2": {"b1": 1.4, "b2": 1.6, "s1": 0.9, "s2": 0.2},
"SDXL": {"b1": 1.3, "b2": 1.4, "s1": 0.9, "s2": 0.2},
}
# active LoRA, if any. Stored globally for easy access when writing metadata on outputs
ACTIVE_LORA = None
ACTIVE_LORA_WEIGHT = 1.0
# experimental. Not applicable for true LoRAs, as they are directly applied to the weights (applicable for e.g. LoCon models)
LORA_DROPOUT = 0.0
LORA_TEXT_ENCODER_FACTOR = 1
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(type=str, nargs="?", default=None, help="text prompt for generation. Leave unset to enter prompt loop mode. Multiple prompts can be separated by ||.", dest="prompt")
parser.add_argument("-ii", "--init-img", type=str, default=None, help="use img2img mode. path to the input image", dest="init_img_path")
parser.add_argument("-st", "--strength", type=float, default=0.75, help="strength of initial image in img2img. 0.0 -> no new information, 1.0 -> only new information", dest="strength")
parser.add_argument("-s","--steps", type=int, default=50, help="number of sampling steps", dest="steps")
parser.add_argument("-sc","--scheduler", type=str, default="mdpms", choices=IMPLEMENTED_SCHEDULERS, help="scheduler used when sampling in the diffusion loop", dest="sched_name")
parser.add_argument("-e", "--ddim-eta", type=float, default=0.0, help="eta adds additional random noise when sampling, only applied on ddim sampler. 0 -> deterministic", dest="ddim_eta")
parser.add_argument("-es","--ddim-eta-seed", type=str, default=None, help="secondary seed for the eta noise with ddim sampler and eta>0", dest="eta_seed")
parser.add_argument("-H","--H", type=int, default=512, help="image height, in pixel space", dest="height")
parser.add_argument("-W","--W", type=int, default=512, help="image width, in pixel space", dest="width")
parser.add_argument("-n", "--n-samples", type=int, default=1, help="how many samples to produce for each given prompt. A.k.a. batch size", dest="n_samples")
parser.add_argument("-seq", "--sequential_samples", action="store_true", help="Run batch in sequence instead of in parallel. Removes VRAM requirement for increased batch sizes, increases processing time.", dest="sequential_samples")
parser.add_argument("-cs", "--scale", type=float, default=9, help="(classifier free) guidance scale (higher values may increse adherence to prompt but decrease 'creativity')", dest="guidance_scale")
parser.add_argument("-S","--seed", type=str, default=None, help="initial noise seed for reproducing/modifying outputs (None will select a random seed)", dest="seed")
parser.add_argument("--unet-full", action='store_false', help="Run diffusion UNET at full precision (fp32). Default is half precision (fp16). Increases memory load.", dest="half")
parser.add_argument("--latents-half", action='store_true', help="Generate half precision latents (fp16). Default is full precision latents (fp32), memory usage will only reduce by <1MB. Outputs will be slightly different.", dest="half_latents")
parser.add_argument("--diff-device", type=str, default=DIFFUSION_DEVICE, help="Device for running diffusion process", dest="diffusion_device")
parser.add_argument("--io-device", type=str, default=IO_DEVICE, help="Device for running text encoding and VAE decoding. Keep on CPU for reduced VRAM load.", dest="io_device")
parser.add_argument("--animate", action="store_true", help="save animation of generation process. Very slow unless --io_device is set to \"cuda\"", dest="animate")
parser.add_argument("-in", "--interpolate", nargs=2, type=str, help="Two image paths for generating an interpolation animation", default=None, dest='interpolation_targets')
parser.add_argument("-inx", "--interpolate_extend", type=float, help="Interpolate beyond the specified images in the latent space by the given factor. Disabled with 0. Unlikely to provide useful results.", default=0, dest='interpolation_extend')
parser.add_argument("--no-check-nsfw", action="store_true", help="Disable NSFW check entirely, slightly speeding up generation.", dest="no_check")
parser.add_argument("-spl", "--safety-processing-level", type=int, default=3, help="Set level of content safety processing. 7/6 functions like the diffusers pipeline default, 5/4/3 applies a local blur, 2/1 does not process images outside of special labels.", dest="safety_processing_level")
parser.add_argument("-pf", "--prompts-file", type=str, help="Path of file containing prompts. One line per prompt.", default=None, dest='prompts_file')
parser.add_argument("-ic", "--image-cycles", type=int, help="Repetition count when using image2image. Will interpolate between multiple prompts when || is used.", default=0, dest='img_cycles')
parser.add_argument("-cni", "--cycle-no-save-individual", action="store_false", help="Disables saving of individual images in image cycle mode.", dest="image_cycle_save_individual")
parser.add_argument("-csi", "--cycle-select-image", action="store_true", help="When cycling with batch sizes >1, manually select one of the outputs as the next input image ('guided evolution')", dest="image_cycle_select")
parser.add_argument("-iz", "--image-zoom", type=int, help="Amount of zoom (pixels cropped per side) for each subsequent img2img cycle", default=0, dest='img_zoom')
parser.add_argument("-ir", "--image-rotate", type=int, help="Amount of rotation (counter-clockwise degrees) for each subsequent img2img cycle", default=0, dest='img_rotation')
parser.add_argument("-it", "--image-translate", type=int, help="Amount of translation (x,y tuple in pixels) for each subsequent img2img cycle", default=None, nargs=2, dest='img_translation')
parser.add_argument("-irc", "--image-rotation-center", type=int, help="Center of rotational axis when applying rotation (0,0 -> top left corner) Default is the image center", default=None, nargs=2, dest='img_center')
parser.add_argument("-ics", "--image-cycle-sharpen", type=float, default=1.0, help="Sharpening factor applied when zooming and/or rotating. Reduces effect of constant image blurring due to resampling. Sharpens at >1.0, softens at <1.0", dest="cycle_sharpen")
parser.add_argument("-icc", "--image-color-correction", action="store_true", help="When cycling images, keep lightness and colorization distribution the same as it was initially (LAB histogram cdf matching). Prevents 'magenta shifting' with multiple cycles.", dest="cycle_color_correction")
parser.add_argument("-cfi", "--cycle-fresh-image", action="store_true", help="When cycling images (-ic), create a fresh image (use text-to-image) in each cycle. Useful for interpolating prompts (especially with fixed seed)", dest="cycle_fresh")
parser.add_argument("-cb", "--cuda-benchmark", action="store_true", help="Perform CUDA benchmark. Should improve throughput when computing on CUDA, but may slightly increase VRAM usage.", dest="cuda_benchmark")
parser.add_argument("-as", "--attention-slice", type=int, default=None, help="Set UNET attention slicing slice size. Also enables VAE slicing and tiling. 0 for recommended head_count//2, 1 for maximum memory savings", dest="attention_slicing")
parser.add_argument("-co", "--cpu-offload", action='store_true', help="Set to enable CPU offloading through accelerate. This should enable compatibility with minimal VRAM at the cost of speed.", dest="cpu_offloading")
parser.add_argument("-gsc","--gs-schedule", type=str, default=None, choices=IMPLEMENTED_GS_SCHEDULES, help="Set a schedule for variable guidance scale. Default (None) corresponds to no schedule.", dest="gs_schedule")
parser.add_argument("-ks","--karras-sigmas", type=bool, default=True, help="Set to have the scheduler use 'Karras Sigmas' if available.", dest="karras_sigmas")
parser.add_argument("-fup","--freeu-parameters", type=float, default=(0,0,0,0), nargs="+", help="Set FreeU parameters b1, b2, s1, s2. Set -1 to autoselect or 0 to disable.", dest="freeu_params")
parser.add_argument("-om","--online-model", type=str, default=None, help="Set an online model id for acquisition from huggingface hub.", dest="online_model")
parser.add_argument("-lm","--local-model", type=str, default=None, help="Set a path to a directory containing local model files (should contain unet and vae dirs, see local install in readme).", dest="local_model")
parser.add_argument("-od","--output-dir", type=str, default=None, help="Set an override for the base output directory. The directory will be created if not already present.", dest="output_dir")
parser.add_argument("-mn","--mix-negative-prompts", action="store_true", help="Mix negative prompts directly into the prompt itself, instead of using them as uncond embeddings.", dest="negative_prompt_mixing")
parser.add_argument("-dnp","--default-negative-prompt", type=str, default="", help="Specify a default negative prompt to use when none are specified.", dest="default_negative")
parser.add_argument("-cls", "--clip-layer-skip", type=int, default=0, help="Skip last n layers of CLIP text encoder. Reduces processing/interpretation level of prompt. Recommended for some custom models", dest="clip_skip_layers")
parser.add_argument("-rnc", "--re-encode", type=str, default=None, help="Re-encode an image or folder using the VAE of the loaded model. Works using latents saved in PNG metadata", dest="re_encode")
parser.add_argument("-sel", "--static-embedding-length", type=int, default=None, help="Process prompts without dynamic length. A value of 77 should reproduce results of previous/other implementations. Switches from embedding concatenation to mixing.", dest="static_length")
parser.add_argument("-mc", "--mix_concat", action="store_true", help="When mixing prompts, perform a concatenation of their embeddings instead of calculating a weighted sum.", dest="mix_mode_concat")
parser.add_argument("-lop", "--lora-path", type=str, default=None, nargs="+", help="Path(s) to LoRA embedding(s). Will be loaded via diffusers attn_procs / lora_diffusion / lora converter.", dest="lora_path")
parser.add_argument("-low", "--lora-weight", type=float, default=[0.8], nargs="+", help="Weight(s) of LoRA embedding(s) loaded via --lora-path. Will wrap around if there are less weights than embeddings.", dest="lora_weight")
parser.add_argument("-losc", "--lora-schedule", type=str, default=None, choices=IMPLEMENTED_GS_SCHEDULES, help="Set a schedule for strength of LoRA-like models (basic LoRAs are not affected). Default (None) corresponds to no schedule.", dest="lora_schedule")
parser.add_argument("-com","--controlnet-model", type=str, default=None, help=f"Set a name, path or hub id pointing to a controlnet model. Known names: {CONTROLNET_SHORTNAMES}", dest="controlnet_model")
parser.add_argument("-coi","--controlnet-input", type=str, default=None, help="Set a path pointing to an input image for the controlnet.", dest="controlnet_input")
parser.add_argument("-cost", "--controlnet-strength", type=float, default=1.0, help="strength (cond scale) for applying a controlnet", dest="controlnet_strength")
parser.add_argument("-cosc","--controlnet-schedule", type=str, default=None, choices=IMPLEMENTED_GS_SCHEDULES, help="Set a schedule for controlnet strength. Default (None) corresponds to no schedule.", dest="controlnet_schedule")
parser.add_argument("-cop","--controlnet-preprocessor", type=str, default=None, choices=IMPLEMENTED_CONTROLNET_PREPROCESSORS, help="Set a preprocessor for controlnet inputs.", dest="controlnet_preprocessor")
parser.add_argument("-gr", "--guidance-rescale", type=float, default=0.66, help="Set the guidance rescale factor 'phi' to improve image exposure distribution. Disable with a value of 0. Authors of arxiv:2305.08891 recommend values between 0.5 and 0.75", dest="guidance_rescale")
parser.add_argument("-spr","--second-pass-resize", type=float, default=1, help="Perform a second pass with image size multiplied by this factor. Initially sampling at reduced size can improve consistency and speed with large resolutions. Enabled for values >1.", dest="second_pass_resize")
parser.add_argument("-sps","--second-pass-steps", type=int, default=50, help="Number of sampling steps at which the new size is used when starting_resize is specified.", dest="second_pass_steps")
parser.add_argument("-spc","--second-pass-controlnet", action="store_true", help="Use the first pass image in the selected controlnet for the second pass (instead of using img2img). For batch sizes >1, only the first image from the first pass will be used (outside of sequential mode).", dest="second_pass_use_controlnet")
return parser.parse_args()
def main():
global DIFFUSION_DEVICE, IO_DEVICE, OFFLOAD_EXEC_DEVICE
global model_id, models_local_dir
global OUTPUT_DIR_BASE, OUTPUTS_DIR, INDIVIDUAL_OUTPUTS_DIR, UNPUB_DIR, ANIMATIONS_DIR
global DEFAULT_NEGATIVE_PROMPT
global ACTIVE_LORA, ACTIVE_LORA_WEIGHT
args = parse_args()
try:
import torch_directml
device_handle = torch_directml.device()
device_names = ("dml","directml")
using_dml = False
if str(args.diffusion_device).lower().strip() in device_names:
args.diffusion_device = device_handle
using_dml = True
if str(args.io_device).lower().strip() in device_names:
args.io_device = device_handle
using_dml = True
if str(OFFLOAD_EXEC_DEVICE).lower().strip() in device_names:
OFFLOAD_EXEC_DEVICE = device_handle
using_dml = using_dml or args.cpu_offloading
if using_dml:
print(f"Using DirectML backend! This feature is currently experimental. It may break, and has been observed to freeze the application with no errors in some configurations.")
# offload may break with a LoRA loaded (application freezes during text encode)
except ImportError:
pass
if args.cpu_offloading:
args.diffusion_device, args.io_device = OFFLOAD_EXEC_DEVICE, OFFLOAD_EXEC_DEVICE
DIFFUSION_DEVICE = args.diffusion_device
IO_DEVICE = args.io_device
DEFAULT_NEGATIVE_PROMPT = args.default_negative
ACTIVE_LORA = args.lora_path
ACTIVE_LORA_WEIGHT = args.lora_weight
args.seed = input_to_seed(args.seed)
args.eta_seed = input_to_seed(args.eta_seed)
if args.online_model is not None:
# set the online model id
model_id = args.online_model
# set local model to None to disable local override
models_local_dir = None
if args.local_model is not None:
# local model dirs function as an override - online id can remain untouched
models_local_dir = args.local_model
if args.output_dir is not None:
OUTPUT_DIR_BASE = args.output_dir
OUTPUTS_DIR = f"{OUTPUT_DIR_BASE}/generated"
ANIMATIONS_DIR = f"{OUTPUT_DIR_BASE}/animate"
INDIVIDUAL_OUTPUTS_DIR = os.path.join(OUTPUTS_DIR, "individual")
UNPUB_DIR = os.path.join(OUTPUTS_DIR, "unpub")
os.makedirs(OUTPUTS_DIR, exist_ok=True)
os.makedirs(INDIVIDUAL_OUTPUTS_DIR, exist_ok=True)
os.makedirs(UNPUB_DIR, exist_ok=True)
os.makedirs(ANIMATIONS_DIR, exist_ok=True)
if args.cuda_benchmark:
torch.backends.cudnn.benchmark = True
# load up models
tokenizer, text_encoder, unet, vae, rate_nsfw = load_models(half_precision=args.half, cpu_offloading=args.cpu_offloading, lora_dropout=LORA_DROPOUT)
if args.no_check:
rate_nsfw = lambda x: False
# create generate function using the loaded models
# if interpolation is requested, run interpolation instead!
if args.interpolation_targets is not None:
# swap places between UNET and VAE to speed up large batch decode. (When offloaded, skip this.)
try:
unet = unet.to(IO_DEVICE)
except NotImplementedError:
pass
try:
vae = vae.to(DIFFUSION_DEVICE)
except NotImplementedError:
pass
create_interpolation(args.interpolation_targets[0], args.interpolation_targets[1], args.steps, vae, args.interpolation_extend)
exit()
if args.re_encode is not None:
try:
unet = unet.to(IO_DEVICE)
except NotImplementedError:
pass
try:
vae = vae.to(DIFFUSION_DEVICE)
except NotImplementedError:
pass
enc_path = Path(args.re_encode)
if not enc_path.exists():
print(f"Selected path does not exist: {enc_path}")
exit()
if enc_path.is_file():
targets = [enc_path]
elif enc_path.is_dir():
targets = [f for f in enc_path.rglob("*.png")]
print(f"Accumulated {len(targets)} target files for re-encode!")
for item in tqdm(targets, desc="Re-encoding"):
try:
encoded = re_encode(item, vae)
meta = load_metadata_from_image(item)
meta["filename"] = str(item)
meta["re-encode"] = f"{models_local_dir} (local)" if using_local_vae else model_id
save_output(p=None, imgs=encoded, argdict=None, data_override=meta)
except KeyboardInterrupt:
exit()
except:
print_exc()
exit()
controlnet_model = load_controlnet(args.controlnet_model,DIFFUSION_DEVICE,args.cpu_offloading)
generator = QuickGenerator(tokenizer,text_encoder,unet,vae,IO_DEVICE,DIFFUSION_DEVICE,rate_nsfw,args.half_latents)
generator.configure(
args.n_samples, args.width, args.height, args.steps, args.guidance_scale, args.seed, args.sched_name, args.ddim_eta, args.eta_seed, args.strength,
args.animate, args.sequential_samples, True, True, args.attention_slicing, True, args.gs_schedule,
args.negative_prompt_mixing, args.clip_skip_layers, args.static_length, args.mix_mode_concat,
controlnet_model, args.controlnet_strength, args.controlnet_preprocessor, args.controlnet_schedule,
args.safety_processing_level, args.guidance_rescale, args.second_pass_resize, args.second_pass_steps, args.second_pass_use_controlnet,
use_karras_sigmas=args.karras_sigmas, lora_schedule=args.lora_schedule, freeu_params=args.freeu_params
)
init_image = None if args.init_img_path is None else Image.open(args.init_img_path).convert("RGB")
controlnet_input = None if args.controlnet_input is None else Image.open(args.controlnet_input).convert("RGB")
if args.img_cycles > 0:
generator.perform_image_cycling(
args.prompt, args.img_cycles, args.image_cycle_save_individual, True, init_image, args.cycle_fresh, args.cycle_color_correction, None,
args.img_zoom, args.img_rotation, args.img_center, args.img_translation, args.cycle_sharpen,
controlnet_input=controlnet_input, select_next_image_in_batch=args.image_cycle_select
)
exit()
if args.prompts_file is not None:
lines = [line.strip() for line in codecs.open(args.prompts_file, "r").readlines()]
for line in tqdm(lines, position=0, desc="Input prompts"):
generator.one_generation(line, init_image, controlnet_input=controlnet_input)
elif args.prompt is not None:
generator.one_generation(args.prompt, init_image, controlnet_input=controlnet_input)
else:
while True:
prompt = input("Prompt> ").strip()
try:
generator.one_generation(prompt, init_image, controlnet_input=controlnet_input)
except KeyboardInterrupt:
pass
except Exception as e:
print_exc()
def recurse_set_xformers(item, max_recursion_depth=16):
if not XFORMERS_ENABLED:
return
if hasattr(item, "set_use_memory_efficient_attention_xformers"):
item.set_use_memory_efficient_attention_xformers(True,None)
recurse_targets = item.__dict__.values() if hasattr(item, "__dict__") else item.values() if isinstance(item, dict) else item if isinstance(item, list) or hasattr(item, "__iter__") else None
if max_recursion_depth > 0 and recurse_targets is not None:
for x in recurse_targets:
recurse_set_xformers(x,max_recursion_depth-1)
def load_models(half_precision=False, unet_only=False, cpu_offloading=False, vae_only=False, lora_override:str=None, lora_weight_override:float=None, lora_dropout:float=0.0):
global using_local_unet, using_local_vae, PROMPT_SUBST, MAX_CLIP_SKIP, ACTIVE_LORA, ACTIVE_LORA_WEIGHT
# re-set substitution list (re-loading model would otherwise re-add custom embedding substitutions)
PROMPT_SUBST = CUSTOM_PROMPT_SUBST
if cpu_offloading:
if not is_accelerate_available():
print("accelerate library is not installed! Unable to utilise CPU offloading!")
cpu_offloading=False
else:
# NOTE: this only offloads parameters, but not buffers by default.
from accelerate import cpu_offload
torch.no_grad() # We don't need gradients where we're going.
tokenizer:transformers_models.clip.tokenization_clip.CLIPTokenizer
text_encoder:transformers_models.clip.modeling_clip.CLIPTextModel
# unet:diffusers_models.unets.unet_2d_condition.UNet2DConditionModel, # new model class location. changed with recent diffusers ver. Keep this commented out for version compatibility for now
vae:diffusers_models.AutoencoderKL
unet_model_id = model_id
vae_model_id = model_id
text_encoder_id = "openai/clip-vit-large-patch14" # sd 1.x default
tokenizer_id = "openai/clip-vit-large-patch14" # sd 1.x default
xl_vae_model_id = "stabilityai/stable-diffusion-xl-base-1.0"
# pipe '|' splits the model path from the subdir here
xl_text_encoder_id = ("stabilityai/stable-diffusion-xl-base-1.0|text_encoder","stabilityai/stable-diffusion-xl-base-1.0|text_encoder_2")
xl_tokenizer_id = ("stabilityai/stable-diffusion-xl-base-1.0|tokenizer","stabilityai/stable-diffusion-xl-base-1.0|tokenizer_2")
use_auth_token_unet=get_huggingface_token()
use_auth_token_vae=get_huggingface_token()
default_local_model_files_per_dir = [["config.json"], ["diffusion_pytorch_model.bin", "diffusion_pytorch_model.safetensors"]]
def dir_has_model_files(dirpath, files_groups=None):
files_groups = files_groups if files_groups is not None else default_local_model_files_per_dir
return all([any([os.path.exists(os.path.join(dirpath, f)) for f in group]) for group in files_groups])
if models_local_dir is not None and len(models_local_dir) > 0:
unet_dir = os.path.join(models_local_dir, "unet")
if dir_has_model_files(unet_dir):
use_auth_token_unet=False
print(f"Using local unet model files! ({models_local_dir})")
unet_model_id = unet_dir
using_local_unet = True
try:
unet_config_path = Path(unet_dir).joinpath("config.json")
assert unet_config_path.exists() and unet_config_path.is_file() # should always be true, we checked for model files when selecting 'local unet'
unet_config = load_json_str(unet_config_path.read_text())
if unet_config["addition_embed_type"] == "text_time":
# somewhat crude SDXL check on the unet config, but this holds true for base SDXL and PGv2.5
vae_model_id = xl_vae_model_id
text_encoder_id = xl_text_encoder_id
tokenizer_id = xl_tokenizer_id
except Exception:
pass
else:
print(f"Using unet '{unet_model_id}' (no local data present at {unet_dir})")
vae_dir = os.path.join(models_local_dir, "vae")
if dir_has_model_files(vae_dir):
use_auth_token_vae=False
print(f"Using local vae model files! ({models_local_dir})")
vae_model_id = vae_dir
using_local_vae = True
else:
print(f"Using vae '{vae_model_id}' (no local data present at {vae_dir})")
text_encoder_dir = os.path.join(models_local_dir, "text_encoder")
text_encoder_2_dir = os.path.join(models_local_dir, "text_encoder_2")
filegroups_text_encoder = [["config.json"], ["pytorch_model.bin", "pytorch_model.safetensors", "model.bin", "model.safetensors"]]
if dir_has_model_files(text_encoder_dir, filegroups_text_encoder):
if dir_has_model_files(text_encoder_2_dir, filegroups_text_encoder):
text_encoder_id = (text_encoder_dir, text_encoder_2_dir)
print(f"Using local text encoder! ({models_local_dir}) [SDXL]")
else:
text_encoder_id = text_encoder_dir
print(f"Using local text encoder! ({models_local_dir})")
else:
if not isinstance(text_encoder_id, tuple):
print(f"Using text encoder '{text_encoder_id}' (no local data present at {text_encoder_dir})")
else:
print(f"Using text encoder '{text_encoder_id[0].split('|')[0]} [SDXL]' (no local data present at {text_encoder_dir})")
tokenizer_dir = os.path.join(models_local_dir, "tokenizer")
tokenizer_2_dir = os.path.join(models_local_dir, "tokenizer_2")
filegroups_tokenizer = [["merges.txt"],["vocab.json"]]
if dir_has_model_files(tokenizer_dir, filegroups_tokenizer):
if dir_has_model_files(tokenizer_2_dir, filegroups_tokenizer):
print(f"Using local tokenizer! ({models_local_dir}) [SDXL]")
tokenizer_id = (tokenizer_dir, tokenizer_2_dir)
else:
print(f"Using local tokenizer! ({models_local_dir})")
tokenizer_id = tokenizer_dir
else:
if not isinstance(tokenizer_id, tuple):
print(f"Using tokenizer '{tokenizer_id}' (no local data present at {tokenizer_dir})")
else:
print(f"Using tokenizer '{tokenizer_id[0].split('|')[0]} [SDXL]' (no local data present at {tokenizer_dir})")
# crude check to find out if a v_prediction model is present. To be replaced by proper "model default config" at some point.
global V_PREDICTION_MODEL
scheduler_config_file_path = os.path.join(models_local_dir, "scheduler/scheduler_config.json")
if os.path.exists(scheduler_config_file_path):
with open(scheduler_config_file_path) as f:
V_PREDICTION_MODEL = '"v_prediction"' in f.read()
else:
V_PREDICTION_MODEL = False
ACTIVE_LORA = lora_override if lora_override is not None else ACTIVE_LORA
ACTIVE_LORA_WEIGHT = lora_weight_override if lora_weight_override is not None else ACTIVE_LORA_WEIGHT if ACTIVE_LORA_WEIGHT is not None else [1.0]
ACTIVE_LORA_WEIGHT = [ACTIVE_LORA_WEIGHT] if isinstance(ACTIVE_LORA_WEIGHT, float) else ACTIVE_LORA_WEIGHT
# to disable LoRA via override, pass "". No override keeps the current LoRA setting to keep re-loading functionality unchanged
if ACTIVE_LORA not in [None, "", []]:
try:
if isinstance(ACTIVE_LORA, str):
ACTIVE_LORA = ACTIVE_LORA.split(";")
if isinstance(ACTIVE_LORA_WEIGHT, str):
ACTIVE_LORA_WEIGHT = [try_float(x,1.0) for x in ACTIVE_LORA_WEIGHT.split(";")]
if len(ACTIVE_LORA_WEIGHT) < len(ACTIVE_LORA): # if there are less weights than embeddings, extend them to the correct length by wrapping around to the first value
ACTIVE_LORA_WEIGHT *= int(len(ACTIVE_LORA)/len(ACTIVE_LORA_WEIGHT))+1
ACTIVE_LORA_WEIGHT = ACTIVE_LORA_WEIGHT[:len(ACTIVE_LORA)]
# used to pass one 'pipe' object to lora_diffusion.patch_pipe: expects 'pipe' object with properties pipe.unet, pipe.text_encoder, pipe.tokenizer
pseudo_pipe_container = lambda **kwargs: type("PseudoPipe", (), kwargs)
active_lora_paths = []
active_lora_weights = []
for lora_item, lora_weight in zip(ACTIVE_LORA, ACTIVE_LORA_WEIGHT):
if not (Path(lora_item).exists() and Path(lora_item).is_file()):
if Path(lora_item+".safetensors").exists() and Path(lora_item+".safetensors").is_file():
lora_item += ".safetensors"
elif Path(lora_item+".pt").exists() and Path(lora_item+".pt").is_file():
lora_item += ".pt"
elif Path(lora_item+".bin").exists() and Path(lora_item+".bin").is_file():
lora_item += ".bin"
else:
tqdm.write(str(ValueError(f"LoRA path {lora_item} could not be parsed to a valid file.")))
continue
if not (lora_item.endswith(".pt") or lora_item.endswith(".safetensors") or lora_item.endswith(".bin")):
tqdm.write(str(ValueError(f"LoRA file {lora_item} should have either '.pt'/'.bin' or '.safetensors' extension, specifying the file format. Assuming .pt for non-safetensors files.")))
continue
active_lora_paths.append(lora_item)
active_lora_weights.append(lora_weight)
ACTIVE_LORA = active_lora_paths
ACTIVE_LORA_WEIGHT = active_lora_weights
def patch_wrapper(unet, text_encoder, tokenizer):
global ACTIVE_LORA, ACTIVE_LORA_WEIGHT
loaded_lora = []
loaded_lora_weights = []
for lora, weight in zip(ACTIVE_LORA, ACTIVE_LORA_WEIGHT):
try:
if patch_worker(unet, text_encoder, tokenizer, lora, weight):
loaded_lora.append(lora)
loaded_lora_weights.append(weight)
except Exception as e:
tqdm.write(f"lora {lora} could not be applied: {e}")
ACTIVE_LORA = loaded_lora
ACTIVE_LORA_WEIGHT = loaded_lora_weights
def patch_worker(unet, text_encoder, tokenizer, lora_item, item_weight:1.0):
try:
path_item = Path(lora_item)
if path_item.suffix == ".safetensors":
from safetensors.torch import load_file #, safe_open
load_data = load_file(path_item)
"""with safe_open(path_item, framework="pt") as handle:
if handle.metadata() is not None:
print(f"metadata for {lora_item}:")
pprint(handle.metadata())"""
else:
load_data = torch.load(path_item)
except ImportError as e:
tqdm.write(f"Could not load LoRa due to missing library: {e}")
return False
except ValueError as e:
print_exc()
tqdm.write(f"Could not load LoRa file: {e}")
return False
encoder_is_sd1 = not isinstance(text_encoder, MultiEncoder) and text_encoder.get_input_embeddings().weight.data.shape[1] == 768
# first, try loading as a diffusers attn_procs (most specific format)
try:
_load_data = {k:v for k,v in load_data.items()} # on SDXL, having attn_procs try to load a convertable lora will cause the dict to end up emptied. pass a shallow copy instead.
sd1_len_keys = [k for k in _load_data.keys() if len(getattr(_load_data[k], "shape", [])) >=2 and 768 == _load_data[k].shape[1]]
is_sd1 = all([("transformer_blocks.0.attn2.processor.to_" in x and "_lora.down.weight" in x) for x in sd1_len_keys]) and len(sd1_len_keys) >= 32
if is_sd1 != encoder_is_sd1: # crude version check, maybe replace with something better
raise ValueError(f"{lora_item} contains weights for {'SD2.x' if encoder_is_sd1 else 'SD1.x'}, but encoder is {'SD1.x' if encoder_is_sd1 else 'SD2.x'}")
unet.load_attn_procs(_load_data)
tqdm.write(f"{lora_item} loaded as a diffusers attn_proc!")
return True
except Exception as e:
# print_exc()
pass
#tqdm.write(f"{lora_item} could not be loaded as diffusers attn_procs")
# second, try loading as lora_diffusers
try:
if not ["<s1>"] in load_data.keys():
raise ValueError(f"embedding is not lora_diffusers.")
elif load_data["<s1>"].shape[0] == 768 != encoder_is_sd1: # crude version check, maybe replace with something better
raise ValueError(f"{lora_item} contains weights for {'SD2.x' if encoder_is_sd1 else 'SD1.x'}, but encoder is {'SD1.x' if encoder_is_sd1 else 'SD2.x'}")
from lora_diffusion import patch_pipe, tune_lora_scale
patch_only_unet = text_encoder is None or tokenizer is None
pseudo_pipe = pseudo_pipe_container(unet=unet,text_encoder=text_encoder,tokenizer=tokenizer)
patch_pipe(pseudo_pipe, lora_item, patch_text=not patch_only_unet, patch_ti=not patch_only_unet)
tune_lora_scale(unet, item_weight)
if not patch_only_unet:
tune_lora_scale(text_encoder, item_weight)
tqdm.write(f"{lora_item} loaded via lora_diffusers!")
return True
except ImportError:
tqdm.write("Unable to load lora_diffusion. Please install via 'pip install git+https://github.com/cloneofsimo/lora.git' to use lora_diffusion embeddings.")
pass
except Exception as e:
# print_exc()
pass
#tqdm.write(f"{lora_item} could not be loaded via lora_diffusion.")
# finally, try to use lora converter to load
try:
sd1_len_keys = [k for k in load_data.keys() if len(getattr(load_data[k], "shape", [])) >=2 and 768 == load_data[k].shape[1] and "lora_down.weight" in k]
#if (len(sd1_len_keys) >= 92) != encoder_is_sd1: # crude version check, maybe replace with something better
# print(f"{lora_item} contains weights for {'SD2.x' if encoder_is_sd1 else 'SD1.x'}, but encoder is {'SD1.x' if encoder_is_sd1 else 'SD2.x'}")
# return False
load_lora_convert(load_data,unet=unet, text_encoder=text_encoder, merge_strength=item_weight, merge_strength_text_encoder=item_weight*LORA_TEXT_ENCODER_FACTOR, lora_dropout=lora_dropout)
tqdm.write(f"{lora_item} loaded via LoRA converter!")
return True
except Exception as e:
print_exc()
pass
#tqdm.write(f"{lora_item} could not be loaded via LoRa converter.")
tqdm.write(f"{lora_item} embedding failed to load using known methods: diffusers attn_proc / lora_diffusion / lora converter")
return False
except ValueError as e:
print(e)
ACTIVE_LORA = None
def patch_wrapper(*args, **kwargs):
pass
else:
ACTIVE_LORA = None
def patch_wrapper(*args, **kwargs):
pass
if not vae_only:
# Load the UNet model for generating the latents.
if half_precision:
unet = UNet2DConditionModel.from_pretrained(unet_model_id, subfolder="unet", use_auth_token=use_auth_token_unet, torch_dtype=torch.float16, revision="fp16")
else:
unet = UNet2DConditionModel.from_pretrained(unet_model_id, subfolder="unet", use_auth_token=use_auth_token_unet)
try:
local_model_path = Path(models_local_dir)
assert local_model_path.exists() and local_model_path.is_dir()
sched_config_path = local_model_path.joinpath("scheduler/scheduler_config.json")
assert sched_config_path.exists() and sched_config_path.is_file()
sched_target = str(load_json_str(sched_config_path.read_text()).get("_class_name","none"))
has_edm_scheduler = sched_target.lower().startswith("edm")
setattr(unet, "edm_scheduler", has_edm_scheduler)
except Exception:
pass
if unet_only:
patch_wrapper(unet,None,None)
if cpu_offloading:
cpu_offload(unet, execution_device=OFFLOAD_EXEC_DEVICE, offload_buffers=True)
recurse_set_xformers(unet)
return unet
# Load the autoencoder model which will be used to decode the latents into image space.
if False:
vae = AutoencoderKL.from_pretrained(vae_model_id, subfolder="vae", use_auth_token=use_auth_token_vae, torch_dtype=torch.float16)
else:
vae = AutoencoderKL.from_pretrained(vae_model_id, subfolder="vae", use_auth_token=use_auth_token_vae)
try: # playground v2.5 can somehow have the VAE end up with 0.13025 as config.scale_factor? scheduler_config.json specifies 0.5. This may just be an issue with 0.27 being a dev build?
vae_path = Path(vae_model_id)
assert vae_path.exists() and vae_path.is_dir()
vae_cfg_file = vae_path.joinpath("config.json")
assert vae_cfg_file.exists() and vae_cfg_file.is_file()
vae_cfg_data = load_json_str(vae_cfg_file.read_text())
scaling_factor = vae_cfg_data.get("scaling_factor", None)
assert scaling_factor is not None and isinstance(scaling_factor, float)
setattr(vae, "scaling_factor_override", scaling_factor)
if vae.config.get("scaling_factor", 0.0) != scaling_factor:
vae.config["scaling_factor"] = scaling_factor
except Exception as e:
pass
if vae_only:
recurse_set_xformers(vae)
return vae
# Load the tokenizer and text encoder to tokenize and encode the text.
#tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14")
#text_encoder = CLIPTextModel.from_pretrained("openai/clip-vit-large-patch14")
if not (isinstance(tokenizer_id, tuple) or isinstance(text_encoder_id, tuple)):
# not SDXL
tokenizer = CLIPTokenizer.from_pretrained(tokenizer_id)
text_encoder = CLIPTextModel.from_pretrained(text_encoder_id)
else:
tokenizer_id = tokenizer_id if isinstance(tokenizer_id, tuple) else (tokenizer_id,)
text_encoder_id = text_encoder_id if isinstance(text_encoder_id, tuple) else (text_encoder_id,)
tokenizers = [CLIPTokenizer.from_pretrained(tok_id) if "|" not in tok_id else CLIPTokenizer.from_pretrained(tok_id.split("|",1)[0], subfolder=tok_id.split("|",1)[1]) for tok_id in tokenizer_id]
text_encoders = [CLIPTextModel.from_pretrained(te_id) if "|" not in te_id else CLIPTextModel.from_pretrained(te_id.split("|",1)[0], subfolder=te_id.split("|",1)[1]) for te_id in text_encoder_id]
if len(tokenizers)==2 and len(text_encoders)==2:
tokenizer = SDXLTokenizer(*tokenizers)
text_encoder = SDXLTextEncoder(*text_encoders)
else: # this case should currently be unreachable. Either the model dir is malformed, or someone released a new diffusion model with 3+ text encoders.
assert len(tokenizer_id) == len(text_encoder_id), f"Tokenizer count {len(tokenizer_id)} must match TextEncoder count {len(text_encoder_id)}!"
tokenizer = MultiTokenizer(*tokenizers)
text_encoder = MultiTextEncoder(*text_encoders)
force_zeros_for_empty_prompt = None
is_SDXL = None
try:
model_index_path = Path(models_local_dir).joinpath("model_index.json")
assert model_index_path.exists()
model_index_data = load_json_str(model_index_path.read_text())
force_zeros_for_empty_prompt = model_index_data.get("force_zeros_for_empty_prompt", None)
index_pipe_class = model_index_data.get("_class_name", None)
if index_pipe_class is not None:
is_SDXL = False if index_pipe_class == "StableDiffusionPipeline" else True if index_pipe_class == "StableDiffusionXLPipeline" else None
except Exception:
pass
# if we do not have / can't parse a model index, we'll have to infer force_zeros_for_empty_prompt and is_SDXL
# this should be True for SDXL-style models (which have multiple encoders) and false for SD1.x/SD2.x models (single encoder)
force_zeros_for_empty_prompt = force_zeros_for_empty_prompt if force_zeros_for_empty_prompt is not None else isinstance(text_encoder, MultiTextEncoder)
is_SDXL = is_SDXL if is_SDXL is not None else isinstance(text_encoder, MultiTextEncoder)
setattr(text_encoder, "force_zeros_for_empty_prompt", force_zeros_for_empty_prompt)
setattr(text_encoder, "is_SDXL", is_SDXL)
if isinstance(text_encoder, MultiTextEncoder):
for enc in text_encoder.get_encoders():
setattr(enc, "force_zeros_for_empty_prompt", force_zeros_for_empty_prompt)
setattr(enc, "is_SDXL", is_SDXL)
try:
if not isinstance(text_encoder, MultiTextEncoder):
MAX_CLIP_SKIP = len(text_encoder.text_model.encoder.layers)
else:
# if we have multiple text encoders, the general clip skip limit will be the lowest common denominator
MAX_CLIP_SKIP = min([len(item.text_model.encoder.layers) for item in text_encoder.get_encoders()])
except Exception:
print_exc()
print("Unable to infer encoder hidden layer count (CLIP skip limit) from the text encoder!")
print(f"Encoder hidden layer count (max CLIP skip): {MAX_CLIP_SKIP}")
#rate_nsfw = get_safety_checker(cpu_offloading=cpu_offloading)
rate_nsfw = SafetyChecker(cpu_offloading=False)
concepts_path = Path(concepts_dir)
available_concepts = [f for f in concepts_path.rglob("*.bin")] + [f for f in concepts_path.rglob("*.pt")]
if len(available_concepts) > 0:
print(f"Adding {len(available_concepts)} Textual Inversion concepts found in {concepts_path}: ")
for item in available_concepts:
if not (isinstance(text_encoder, MultiTextEncoder) or isinstance(tokenizer, MultiTokenizer)):
try:
load_learned_embed_in_clip(item, text_encoder, tokenizer)
except Exception as e:
print(f"Loading concept from {item} failed: {e}")
else:
exceptions = []
loaded_encoder_idx = []
for enc,tok,idx in zip(text_encoder.get_encoders(), tokenizer.get_encoders(), range(len(text_encoder.get_encoders()))):
try:
load_learned_embed_in_clip(item, enc, tok)
loaded_encoder_idx.append(idx+1)
except Exception as e:
exceptions.append(e)
if len(loaded_encoder_idx) == 0:
print(f" (failed)")
else:
print(f" [>te_{loaded_encoder_idx[0] if len(loaded_encoder_idx) == 1 else loaded_encoder_idx}]", end="")
print("")
patch_wrapper(unet,text_encoder,tokenizer)
# text encoder should be offloaded after adding custom embeddings to ensure that every embedding is actually on the same device.
if cpu_offloading:
cpu_offload(unet, execution_device=OFFLOAD_EXEC_DEVICE, offload_buffers=True)
cpu_offload(vae, execution_device=OFFLOAD_EXEC_DEVICE, offload_buffers=True)
if not isinstance(text_encoder, MultiTextEncoder):
encoders = [text_encoder]
else:
encoders = text_encoder.get_encoders()
for item in encoders:
if not MODEL_OFFLOAD_TEXT_ENCODERS:
cpu_offload(item, execution_device=OFFLOAD_EXEC_DEVICE, offload_buffers=True)
else:
item.to("cpu")
setattr(item, "_MODEL_OFFLOAD_DEVICE", OFFLOAD_EXEC_DEVICE)
# due to the recursive attribute search approach of recurse_set_xformers, it already works with MultiTextEncoder/MultiTokenizer
recurse_set_xformers([tokenizer, text_encoder, unet, vae, rate_nsfw])
return tokenizer, text_encoder, unet, vae, rate_nsfw
# LoRA forward hook input processing: attempt to extract the actual input tensor for the block forward from the input tuple.
def hook_acquire_input(input):
if isinstance(input, tuple) and len(input) == 1:
input = input[0]
elif isinstance(input, tuple) and len(input) > 1:
tensor_input = [x for x in input if isinstance(x,torch.Tensor)]
extra_input = [x for x in input if not isinstance(x,torch.Tensor)]
if len(tensor_input) !=1 :
raise ValueError(f"Convolutional LoRA block received {len(tensor_input)} tensors. This should be 1. Unsupported format?")
input = tensor_input[0]
if len(extra_input) == 1 and isinstance(extra_input, float) and extra_input[0] != 1.0 and not globals().get("_PRINTED_LORA_CONV_FLOAT_INPUT_INFO", False):
print(f"Convolutional LoRA block receiving extra float input {extra_input[0]} != 1.0 - Possible alpha value? This input will will be ignored.")
global _PRINTED_LORA_CONV_FLOAT_INPUT_INFO
_PRINTED_LORA_CONV_FLOAT_INPUT_INFO = True
elif len(extra_input) > 1 and not globals().get("_PRINTED_LORA_CONV_EXTRA_INPUT_INFO", False):
print(f"Convolutional LoRA block receiving extra input(s) of type(s) {[type(x) for x in extra_input]}. Unsupported format?")
global _PRINTED_LORA_CONV_EXTRA_INPUT_INFO
_PRINTED_LORA_CONV_EXTRA_INPUT_INFO = True
elif not isinstance(input, torch.Tensor):
raise ValueError(f"Convolutional LoRA block received input of type {type(tensor_input)}. Expected tuple of one tensor.")
return input
# take state dict, apply LoRA to unet and text encoder (at strength)
def load_lora_convert(state_dict, unet=None, text_encoder=None, merge_strength:float=0.75, merge_strength_text_encoder:float=None, lora_dropout:float=0.0):
# adapted from https://github.com/haofanwang/diffusers/blob/75501a37157da4968291a7929bb8cb374eb57f22/scripts/convert_lora_safetensor_to_diffusers.py
LORA_PREFIX_TEXT_ENCODER = "lora_te"
LORA_PREFIX_UNET = "lora_unet"
lora_dropout = max(0, lora_dropout)
lora_dropout = min(1, lora_dropout)
dropout = torch.nn.Dropout(p=lora_dropout)
merge_strength_text_encoder = merge_strength_text_encoder if merge_strength_text_encoder is not None else merge_strength
visited = []
unparsed_keys = 0
parsed_keys = 0
null_keys = 0
if isinstance(text_encoder, SDXLTextEncoder):
# we have an SDXL model.
try:
keymap_path = Path(__file__).parent.joinpath("SDXL_unet_key_map.tsv")
sdxl_unet_key_map_str = keymap_path.read_text().replace("\r","\n").replace("\n\n","\n") # \r\n -> \n, \r -> \n. if line endings were transformed, return them to form.
# we already have a string hash function here, should be good enough to serve as a simple validation checksum.
expected_sdxl_map_seed_hash = 331545443627423047
sdxl_map_seed_hash = hash_str_to_seed(sdxl_unet_key_map_str)
if sdxl_map_seed_hash != expected_sdxl_map_seed_hash:
print(f"NOTE: SDXL unet key map {keymap_path} hash: {sdxl_map_seed_hash} does not match the expected value.")
sdxl_unet_map_lines = [(x.split("\t")) for x in sdxl_unet_key_map_str.split("\n") if not x.strip() == ""]
sdxl_unet_keymap = {x[0].replace(".","_").strip():x[1].strip() for x in sdxl_unet_map_lines}
if len(sdxl_unet_keymap.keys()) < len(sdxl_unet_map_lines):
print(f"NOTE: {len(sdxl_unet_map_lines)} unet keymap entries yielded a map of {len(sdxl_unet_keymap.keys())} keys.")
except Exception as e:
print(f"Unable to retrieve SDXL unet key map: {e}")
sdxl_unet_keymap = {}
else:
sdxl_unet_keymap = {}
for key in state_dict:
curr_layer, curr_model = None, None
alpha_scale = 1.0
# ignore alpha keys when checking them directly. We could ignore alpha keys entirely, but this would force the end-user to scale the merge strength when alpha != model_dim
if ".alpha" in key or key in visited:
continue
# when visiting a non-alpha key, check if it is accompanied by a respective alpha key.
alpha_value = state_dict.get(f"{key.rsplit('.',2)[0]}.alpha",None)
if alpha_value is not None:
try:
# position of the lora dim in the weight shape depends on if we're looking at a lora_down or a lora_up key.
# thus far, alphas were only found in 'lora_down/lora_up' data of inspected LoRA/extra-model architectures. This may fail for novel architectures which also use alpha values in keys.
lora_dim = state_dict[key].shape[0 if "lora_down" in key else 1]
alpha_scale = alpha_value / lora_dim
except Exception as e:
print(f"Found alpha key for LoRA key {key}, but failed to derive scaling factor: {e}")
if "text" in key and LORA_PREFIX_TEXT_ENCODER in key:
_merge_strength = merge_strength_text_encoder * alpha_scale
if not isinstance(text_encoder, MultiTextEncoder):
layer_infos = key.split(".")[0].split(LORA_PREFIX_TEXT_ENCODER + "_")[-1].split("_")
curr_layer = text_encoder
curr_model = text_encoder
else:
# for SDXL, the two encoders will have lora keys with prefixes lora_te1_(...) and lora_te1_(...) as opposed to lora_te_(...) for single-text_encoder models
for i, te in enumerate(text_encoder.get_encoders()):
# for every text encoder we have, we check if it matches the key index. Crude, we could read the index from the key with a regex directly.
current_prefix = f"{LORA_PREFIX_TEXT_ENCODER}{i+1}"
if current_prefix in key:
layer_infos = key.split(".")[0].split(current_prefix + "_")[-1].split("_")
curr_layer = te
curr_model = te
# break # we could break here, and save ourselves from checking one other string with SDXL.
# In the unlikely scenario of us having 10+ text encoders, breaking can yield incorrect results unless we checked the range in reverse.
if text_encoder is None:
unparsed_keys += 1
continue
elif LORA_PREFIX_UNET in key:
#_key = key.replace("_middle_block_", "_mid_block_").replace("_input_blocks_", "_down_blocks_").replace("_output_blocks_", "_up_blocks_")
layer_infos = key.split(".")[0].split(LORA_PREFIX_UNET + "_")[-1].split("_")
curr_layer = unet
curr_model = unet
_merge_strength = merge_strength * alpha_scale
if unet is None:
unparsed_keys += 1
continue
else:
print(f"skipping unknown LoRA key {key}") # -> {state_dict[key]}")
unparsed_keys += 1
continue
# there's not much we can do to apply a zero-dim tensor.
if state_dict[key].dim() == 0:
unparsed_keys += 1
null_keys += 1
continue
# find the target layer
try:
temp_name = layer_infos.pop(0)
while len(layer_infos) > -1:
try:
curr_layer = curr_layer.__getattr__(temp_name)
if len(layer_infos) > 0:
temp_name = layer_infos.pop(0)
elif len(layer_infos) == 0:
break
except Exception:
if len(temp_name) > 0:
temp_name += "_" + layer_infos.pop(0)
else:
temp_name = layer_infos.pop(0)
except Exception as e:
if key.startswith(LORA_PREFIX_UNET+"_") and len(sdxl_unet_keymap) > 0:
key_in_unet = key[len(LORA_PREFIX_UNET+"_"):].split(".")[0]
try:
assert key_in_unet in sdxl_unet_keymap, "Unable to resolve key via SDXL unet keymap"
curr_layer = retrieve_unet_layer_via_map(curr_model, sdxl_unet_keymap[key_in_unet])
except Exception as e:
print(f"Parsing {key_in_unet} via unet keymap failed: {e}")
unparsed_keys += 1
continue
else:
print(f"LoRA converter keyparse {e} | {key} | {type(curr_layer) if not hasattr(curr_layer, 'weight') else curr_layer.weight.data.shape} | {state_dict[key].shape}")
unparsed_keys += 1
continue
if not ("lora_up" in key or "lora_down" in key):
if state_dict[key].shape == (1,) and state_dict[key][0] in (0.0,):
unparsed_keys += 1
null_keys += 1
if not globals().get("_PRINTED_LORA_NULL_KEY_INFO", False):
print("'zero' item found in LoRA state dict (single element tensor with 0 value). Treating as null and ignoring.")
global _PRINTED_LORA_NULL_KEY_INFO
_PRINTED_LORA_NULL_KEY_INFO = True
continue
elif state_dict[key].squeeze().dim() == 1:
if isinstance(curr_layer, torch.nn.Linear):
#curr_in_width = curr_layer.weight.data.shape[1]
#curr_out_width = curr_layer.weight.data.shape[0]
curr_in_width = curr_layer.in_features
curr_out_width = curr_layer.out_features
elif isinstance(curr_layer, torch.nn.Conv2d):
curr_in_width = curr_layer.in_channels
curr_out_width = curr_layer.out_channels
else:
print(f"LoRA Key {key} -> {state_dict[key].shape} (presumed to be bias data) directed at a layer which is neither Linear nor Conv2d: {type(curr_layer)}. Unable to parse.")
unparsed_keys += 1
continue
bias_width = state_dict[key].squeeze().shape[0]
# attempt to find the .on_input key of the same key name root. If it is present, place the bias at the input.
on_input = any([isinstance(k,str) and k.endswith(".on_input") and k.startswith(key.rsplit(".",1)[0]) for k in state_dict.keys()])
on_output = any([isinstance(k,str) and k.endswith(".on_output") and k.startswith(key.rsplit(".",1)[0]) for k in state_dict.keys()])
# for some reason, indicator keys seem to be swapped. For layers with non-equal input and output widths, the on_input key is set, but the weights only match the output width.
# on layers with equal input and output widths, this will fail to be caught. As swapping them moves us from width mismatches on a lot of layers to all widths matching, we will consider this correct.
temp = on_input
on_input = on_output
on_output = temp
if bias_width not in (curr_in_width, curr_out_width):
unparsed_keys += 1
print(f"LoRA Key {key} -> {type(state_dict[key]) if not isinstance(state_dict[key], torch.Tensor) else state_dict[key].shape} (presumed to be multiplicative bias) matches neither the input width, nor the output with of the current layer ({curr_in_width} -> {curr_out_width}). Unable to parse.")
if on_input or on_output:
print(f"State dict indicates that the LoRA key was intended for the layer {'input and output (?!)' if on_input and on_output else 'input' if on_input else 'output'}")
continue
"""elif curr_in_width == curr_out_width:
unparsed_keys += 1
# If the key name contains a placement clue, this check may be amended.
print(f"LoRA Key {key} -> {type(state_dict[key]) if not isinstance(state_dict[key], torch.Tensor) else state_dict[key].shape} (presumed to be multiplicative bias) matches both the input and output width of the current layer. Unable to parse.")
continue"""
else:
if on_input or on_output:
if on_input and not curr_in_width==bias_width:
print(f"LoRA Key {key} is targeted at the input of {curr_layer}, but channel widths do not match! ({bias_width} != {curr_in_width})")
on_input = False
if on_output and not curr_out_width==bias_width:
print(f"LoRA Key {key} is targeted at the output of {curr_layer}, but channel widths do not match! ({bias_width} != {curr_out_width})")
on_output = False
elif not curr_in_width == curr_out_width:
# if the bias only matches one of either the input or output of the target module in width, place it there.