InvokeAI 2.2.4.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. backend/__init__.py +0 -0
  2. backend/invoke_ai_web_server.py +1724 -0
  3. backend/modules/__init__.py +0 -0
  4. backend/modules/create_cmd_parser.py +55 -0
  5. backend/modules/get_canvas_generation_mode.py +117 -0
  6. backend/modules/parameters.py +71 -0
  7. backend/modules/parse_seed_weights.py +47 -0
  8. configs/INITIAL_MODELS.yaml +80 -0
  9. configs/models.yaml.example +29 -0
  10. configs/sd-concepts.txt +803 -0
  11. configs/stable-diffusion/v1-finetune.yaml +110 -0
  12. configs/stable-diffusion/v1-finetune_style.yaml +103 -0
  13. configs/stable-diffusion/v1-inference.yaml +79 -0
  14. configs/stable-diffusion/v1-inpainting-inference.yaml +79 -0
  15. configs/stable-diffusion/v1-m1-finetune.yaml +110 -0
  16. docs/assets/invoke-web-server-1.png +0 -0
  17. docs/assets/invoke_ai_banner.png +0 -0
  18. invokeai-2.2.4.5.data/data/assets/caution.png +0 -0
  19. invokeai-2.2.4.5.data/data/configs/INITIAL_MODELS.yaml +80 -0
  20. invokeai-2.2.4.5.data/data/configs/models.yaml.example +29 -0
  21. invokeai-2.2.4.5.data/data/configs/sd-concepts.txt +803 -0
  22. invokeai-2.2.4.5.data/data/configs/stable-diffusion/v1-finetune.yaml +110 -0
  23. invokeai-2.2.4.5.data/data/configs/stable-diffusion/v1-finetune_style.yaml +103 -0
  24. invokeai-2.2.4.5.data/data/configs/stable-diffusion/v1-inference.yaml +79 -0
  25. invokeai-2.2.4.5.data/data/configs/stable-diffusion/v1-inpainting-inference.yaml +79 -0
  26. invokeai-2.2.4.5.data/data/configs/stable-diffusion/v1-m1-finetune.yaml +110 -0
  27. invokeai-2.2.4.5.data/data/frontend/dist/assets/Inter-Bold.790c108b.ttf +0 -0
  28. invokeai-2.2.4.5.data/data/frontend/dist/assets/Inter.b9a8e5e2.ttf +0 -0
  29. invokeai-2.2.4.5.data/data/frontend/dist/assets/favicon.0d253ced.ico +0 -0
  30. invokeai-2.2.4.5.data/data/frontend/dist/assets/index-legacy-b741077f.js +48 -0
  31. invokeai-2.2.4.5.data/data/frontend/dist/assets/index.0dadf5d0.css +1 -0
  32. invokeai-2.2.4.5.data/data/frontend/dist/assets/index.21ff81eb.js +625 -0
  33. invokeai-2.2.4.5.data/data/frontend/dist/assets/logo.13003d72.png +0 -0
  34. invokeai-2.2.4.5.data/data/frontend/dist/assets/polyfills-legacy-dde3a68a.js +4 -0
  35. invokeai-2.2.4.5.data/data/frontend/dist/assets/polyfills.1ff60148.js +1 -0
  36. invokeai-2.2.4.5.data/data/frontend/dist/index.html +23 -0
  37. invokeai-2.2.4.5.data/data/scripts/configure_invokeai.py +797 -0
  38. invokeai-2.2.4.5.data/data/scripts/create_root.py +43 -0
  39. invokeai-2.2.4.5.data/data/scripts/dream.py +10 -0
  40. invokeai-2.2.4.5.data/data/scripts/images2prompt.py +30 -0
  41. invokeai-2.2.4.5.data/data/scripts/invoke.py +5 -0
  42. invokeai-2.2.4.5.data/data/scripts/legacy_api.py +681 -0
  43. invokeai-2.2.4.5.data/data/scripts/load_models.py +12 -0
  44. invokeai-2.2.4.5.data/data/scripts/merge_embeddings.py +124 -0
  45. invokeai-2.2.4.5.data/data/scripts/preload_models.py +12 -0
  46. invokeai-2.2.4.5.data/data/scripts/sd-metadata.py +22 -0
  47. invokeai-2.2.4.5.dist-info/LICENSE +21 -0
  48. invokeai-2.2.4.5.dist-info/LICENSE-ModelWeights.txt +294 -0
  49. invokeai-2.2.4.5.dist-info/METADATA +288 -0
  50. invokeai-2.2.4.5.dist-info/RECORD +169 -0
  51. invokeai-2.2.4.5.dist-info/WHEEL +5 -0
  52. invokeai-2.2.4.5.dist-info/entry_points.txt +4 -0
  53. invokeai-2.2.4.5.dist-info/top_level.txt +6 -0
  54. invokeai-2.2.4.5.dist-info/zip-safe +1 -0
  55. ldm/__init__.py +0 -0
  56. ldm/data/__init__.py +0 -0
  57. ldm/data/base.py +31 -0
  58. ldm/data/imagenet.py +466 -0
  59. ldm/data/lsun.py +126 -0
  60. ldm/data/personalized.py +202 -0
  61. ldm/data/personalized_style.py +169 -0
  62. ldm/generate.py +1159 -0
  63. ldm/invoke/CLI.py +960 -0
  64. ldm/invoke/__init__.py +3 -0
  65. ldm/invoke/args.py +1257 -0
  66. ldm/invoke/concepts_lib.py +169 -0
  67. ldm/invoke/conditioning.py +289 -0
  68. ldm/invoke/devices.py +27 -0
  69. ldm/invoke/generator/__init__.py +4 -0
  70. ldm/invoke/generator/base.py +335 -0
  71. ldm/invoke/generator/embiggen.py +501 -0
  72. ldm/invoke/generator/img2img.py +97 -0
  73. ldm/invoke/generator/inpaint.py +358 -0
  74. ldm/invoke/generator/omnibus.py +175 -0
  75. ldm/invoke/generator/txt2img.py +88 -0
  76. ldm/invoke/generator/txt2img2img.py +182 -0
  77. ldm/invoke/globals.py +31 -0
  78. ldm/invoke/image_util.py +70 -0
  79. ldm/invoke/log.py +66 -0
  80. ldm/invoke/model_cache.py +449 -0
  81. ldm/invoke/patchmatch.py +44 -0
  82. ldm/invoke/pngwriter.py +113 -0
  83. ldm/invoke/prompt_parser.py +655 -0
  84. ldm/invoke/readline.py +442 -0
  85. ldm/invoke/restoration/__init__.py +4 -0
  86. ldm/invoke/restoration/base.py +38 -0
  87. ldm/invoke/restoration/codeformer.py +103 -0
  88. ldm/invoke/restoration/codeformer_arch.py +276 -0
  89. ldm/invoke/restoration/gfpgan.py +84 -0
  90. ldm/invoke/restoration/outcrop.py +108 -0
  91. ldm/invoke/restoration/outpaint.py +92 -0
  92. ldm/invoke/restoration/realesrgan.py +90 -0
  93. ldm/invoke/restoration/vqgan_arch.py +435 -0
  94. ldm/invoke/seamless.py +30 -0
  95. ldm/invoke/server.py +282 -0
  96. ldm/invoke/server_legacy.py +246 -0
  97. ldm/invoke/txt2mask.py +136 -0
  98. ldm/lr_scheduler.py +143 -0
  99. ldm/models/__init__.py +0 -0
  100. ldm/models/autoencoder.py +596 -0
  101. ldm/models/diffusion/__init__.py +0 -0
  102. ldm/models/diffusion/classifier.py +355 -0
  103. ldm/models/diffusion/cross_attention_control.py +443 -0
  104. ldm/models/diffusion/cross_attention_map_saving.py +95 -0
  105. ldm/models/diffusion/ddim.py +111 -0
  106. ldm/models/diffusion/ddpm.py +2263 -0
  107. ldm/models/diffusion/ksampler.py +312 -0
  108. ldm/models/diffusion/plms.py +146 -0
  109. ldm/models/diffusion/sampler.py +450 -0
  110. ldm/models/diffusion/shared_invokeai_diffusion.py +244 -0
  111. ldm/modules/__init__.py +0 -0
  112. ldm/modules/attention.py +274 -0
  113. ldm/modules/diffusionmodules/__init__.py +0 -0
  114. ldm/modules/diffusionmodules/model.py +894 -0
  115. ldm/modules/diffusionmodules/openaimodel.py +1036 -0
  116. ldm/modules/diffusionmodules/util.py +314 -0
  117. ldm/modules/distributions/__init__.py +0 -0
  118. ldm/modules/distributions/distributions.py +104 -0
  119. ldm/modules/ema.py +88 -0
  120. ldm/modules/embedding_manager.py +413 -0
  121. ldm/modules/encoders/__init__.py +0 -0
  122. ldm/modules/encoders/modules.py +772 -0
  123. ldm/modules/image_degradation/__init__.py +6 -0
  124. ldm/modules/image_degradation/bsrgan.py +846 -0
  125. ldm/modules/image_degradation/bsrgan_light.py +751 -0
  126. ldm/modules/image_degradation/utils_image.py +1024 -0
  127. ldm/modules/losses/__init__.py +1 -0
  128. ldm/modules/losses/contperceptual.py +175 -0
  129. ldm/modules/losses/vqperceptual.py +240 -0
  130. ldm/modules/x_transformer.py +752 -0
  131. ldm/simplet2i.py +13 -0
  132. ldm/util.py +264 -0
  133. scripts/configure_invokeai.py +797 -0
  134. scripts/create_root.py +43 -0
  135. scripts/dream.py +10 -0
  136. scripts/images2prompt.py +30 -0
  137. scripts/invoke.py +5 -0
  138. scripts/legacy_api.py +681 -0
  139. scripts/load_models.py +12 -0
  140. scripts/merge_embeddings.py +124 -0
  141. scripts/orig_scripts/img2img.py +296 -0
  142. scripts/orig_scripts/inpaint.py +98 -0
  143. scripts/orig_scripts/knn2img.py +397 -0
  144. scripts/orig_scripts/sample_diffusion.py +313 -0
  145. scripts/orig_scripts/train_searcher.py +147 -0
  146. scripts/orig_scripts/txt2img.py +324 -0
  147. scripts/preload_models.py +12 -0
  148. scripts/sd-metadata.py +22 -0
  149. vendored/clip/clip/__init__.py +1 -0
  150. vendored/clip/clip/bpe_simple_vocab_16e6.txt.gz +0 -0
  151. vendored/clip/clip/clip.py +237 -0
  152. vendored/clip/clip/model.py +436 -0
  153. vendored/clip/clip/simple_tokenizer.py +132 -0
  154. vendored/clip/hubconf.py +42 -0
  155. vendored/clip/setup.py +21 -0
  156. vendored/clip/tests/test_consistency.py +25 -0
  157. vendored/clipseg/datasets/coco_wrapper.py +99 -0
  158. vendored/clipseg/datasets/pascal_zeroshot.py +60 -0
  159. vendored/clipseg/datasets/pfe_dataset.py +129 -0
  160. vendored/clipseg/datasets/phrasecut.py +380 -0
  161. vendored/clipseg/datasets/utils.py +68 -0
  162. vendored/clipseg/evaluation_utils.py +292 -0
  163. vendored/clipseg/general_utils.py +272 -0
  164. vendored/clipseg/metrics.py +271 -0
  165. vendored/clipseg/models/clipseg.py +538 -0
  166. vendored/clipseg/models/vitseg.py +286 -0
  167. vendored/clipseg/score.py +453 -0
  168. vendored/clipseg/setup.py +30 -0
  169. vendored/clipseg/training.py +266 -0
@@ -0,0 +1,182 @@
1
+ '''
2
+ ldm.invoke.generator.txt2img inherits from ldm.invoke.generator
3
+ '''
4
+
5
+ import torch
6
+ import numpy as np
7
+ import math
8
+ import gc
9
+ from ldm.invoke.generator.base import Generator
10
+ from ldm.models.diffusion.ddim import DDIMSampler
11
+ from ldm.invoke.generator.omnibus import Omnibus
12
+ from ldm.models.diffusion.shared_invokeai_diffusion import InvokeAIDiffuserComponent
13
+ from PIL import Image
14
+
15
+ class Txt2Img2Img(Generator):
16
+ def __init__(self, model, precision):
17
+ super().__init__(model, precision)
18
+ self.init_latent = None # for get_noise()
19
+
20
+ @torch.no_grad()
21
+ def get_make_image(self,prompt,sampler,steps,cfg_scale,ddim_eta,
22
+ conditioning,width,height,strength,step_callback=None,**kwargs):
23
+ """
24
+ Returns a function returning an image derived from the prompt and the initial image
25
+ Return value depends on the seed at the time you call it
26
+ kwargs are 'width' and 'height'
27
+ """
28
+ uc, c, extra_conditioning_info = conditioning
29
+ scale_dim = min(width, height)
30
+ scale = 512 / scale_dim
31
+
32
+ init_width = math.ceil(scale * width / 64) * 64
33
+ init_height = math.ceil(scale * height / 64) * 64
34
+
35
+ @torch.no_grad()
36
+ def make_image(x_T):
37
+
38
+ shape = [
39
+ self.latent_channels,
40
+ init_height // self.downsampling_factor,
41
+ init_width // self.downsampling_factor,
42
+ ]
43
+
44
+ sampler.make_schedule(
45
+ ddim_num_steps=steps, ddim_eta=ddim_eta, verbose=False
46
+ )
47
+
48
+ #x = self.get_noise(init_width, init_height)
49
+ x = x_T
50
+
51
+ if self.free_gpu_mem and self.model.model.device != self.model.device:
52
+ self.model.model.to(self.model.device)
53
+
54
+ samples, _ = sampler.sample(
55
+ batch_size = 1,
56
+ S = steps,
57
+ x_T = x,
58
+ conditioning = c,
59
+ shape = shape,
60
+ verbose = False,
61
+ unconditional_guidance_scale = cfg_scale,
62
+ unconditional_conditioning = uc,
63
+ eta = ddim_eta,
64
+ img_callback = step_callback,
65
+ extra_conditioning_info = extra_conditioning_info
66
+ )
67
+
68
+ print(
69
+ f"\n>> Interpolating from {init_width}x{init_height} to {width}x{height} using DDIM sampling"
70
+ )
71
+
72
+ # resizing
73
+ samples = torch.nn.functional.interpolate(
74
+ samples,
75
+ size=(height // self.downsampling_factor, width // self.downsampling_factor),
76
+ mode="bilinear"
77
+ )
78
+
79
+ t_enc = int(strength * steps)
80
+ ddim_sampler = DDIMSampler(self.model, device=self.model.device)
81
+ ddim_sampler.make_schedule(
82
+ ddim_num_steps=steps, ddim_eta=ddim_eta, verbose=False
83
+ )
84
+
85
+ z_enc = ddim_sampler.stochastic_encode(
86
+ samples,
87
+ torch.tensor([t_enc-1]).to(self.model.device),
88
+ noise=self.get_noise(width,height,False)
89
+ )
90
+
91
+ # decode it
92
+ samples = ddim_sampler.decode(
93
+ z_enc,
94
+ c,
95
+ t_enc,
96
+ img_callback = step_callback,
97
+ unconditional_guidance_scale=cfg_scale,
98
+ unconditional_conditioning=uc,
99
+ extra_conditioning_info=extra_conditioning_info,
100
+ all_timesteps_count=steps
101
+ )
102
+
103
+ if self.free_gpu_mem:
104
+ self.model.model.to('cpu')
105
+ self.model.cond_stage_model.device = 'cpu'
106
+ self.model.cond_stage_model.to('cpu')
107
+ gc.collect()
108
+ torch.cuda.empty_cache()
109
+
110
+ return self.sample_to_image(samples)
111
+
112
+ # in the case of the inpainting model being loaded, the trick of
113
+ # providing an interpolated latent doesn't work, so we transiently
114
+ # create a 512x512 PIL image, upscale it, and run the inpainting
115
+ # over it in img2img mode. Because the inpaing model is so conservative
116
+ # it doesn't change the image (much)
117
+ def inpaint_make_image(x_T):
118
+ omnibus = Omnibus(self.model,self.precision)
119
+ result = omnibus.generate(
120
+ prompt,
121
+ sampler=sampler,
122
+ width=init_width,
123
+ height=init_height,
124
+ step_callback=step_callback,
125
+ steps = steps,
126
+ cfg_scale = cfg_scale,
127
+ ddim_eta = ddim_eta,
128
+ conditioning = conditioning,
129
+ **kwargs
130
+ )
131
+ assert result is not None and len(result)>0,'** txt2img failed **'
132
+ image = result[0][0]
133
+ interpolated_image = image.resize((width,height),resample=Image.Resampling.LANCZOS)
134
+ print(kwargs.pop('init_image',None))
135
+ result = omnibus.generate(
136
+ prompt,
137
+ sampler=sampler,
138
+ init_image=interpolated_image,
139
+ width=width,
140
+ height=height,
141
+ seed=result[0][1],
142
+ step_callback=step_callback,
143
+ steps = steps,
144
+ cfg_scale = cfg_scale,
145
+ ddim_eta = ddim_eta,
146
+ conditioning = conditioning,
147
+ **kwargs
148
+ )
149
+ return result[0][0]
150
+
151
+ if sampler.uses_inpainting_model():
152
+ return inpaint_make_image
153
+ else:
154
+ return make_image
155
+
156
+ # returns a tensor filled with random numbers from a normal distribution
157
+ def get_noise(self,width,height,scale = True):
158
+ # print(f"Get noise: {width}x{height}")
159
+ if scale:
160
+ trained_square = 512 * 512
161
+ actual_square = width * height
162
+ scale = math.sqrt(trained_square / actual_square)
163
+ scaled_width = math.ceil(scale * width / 64) * 64
164
+ scaled_height = math.ceil(scale * height / 64) * 64
165
+ else:
166
+ scaled_width = width
167
+ scaled_height = height
168
+
169
+ device = self.model.device
170
+ if self.use_mps_noise or device.type == 'mps':
171
+ return torch.randn([1,
172
+ self.latent_channels,
173
+ scaled_height // self.downsampling_factor,
174
+ scaled_width // self.downsampling_factor],
175
+ device='cpu').to(device)
176
+ else:
177
+ return torch.randn([1,
178
+ self.latent_channels,
179
+ scaled_height // self.downsampling_factor,
180
+ scaled_width // self.downsampling_factor],
181
+ device=device)
182
+
ldm/invoke/globals.py ADDED
@@ -0,0 +1,31 @@
1
+ '''
2
+ ldm.invoke.globals defines a small number of global variables that would
3
+ otherwise have to be passed through long and complex call chains.
4
+
5
+ It defines a Namespace object named "Globals" that contains
6
+ the attributes:
7
+
8
+ - root - the root directory under which "models" and "outputs" can be found
9
+ - initfile - path to the initialization file
10
+ - try_patchmatch - option to globally disable loading of 'patchmatch' module
11
+ '''
12
+
13
+ import os
14
+ import os.path as osp
15
+ from argparse import Namespace
16
+
17
+ Globals = Namespace()
18
+
19
+ # This is usually overwritten by the command line and/or environment variables
20
+ if os.environ.get('INVOKEAI_ROOT'):
21
+ Globals.root = osp.abspath(os.environ.get('INVOKEAI_ROOT'))
22
+ elif os.environ.get('VIRTUAL_ENV'):
23
+ Globals.root = osp.abspath(osp.join(os.environ.get('VIRTUAL_ENV'), '..'))
24
+ else:
25
+ Globals.root = osp.abspath(osp.expanduser('~/invokeai'))
26
+
27
+ # Where to look for the initialization file
28
+ Globals.initfile = 'invokeai.init'
29
+
30
+ # Try loading patchmatch
31
+ Globals.try_patchmatch = True
@@ -0,0 +1,70 @@
1
+ from math import sqrt, floor, ceil
2
+ from PIL import Image
3
+
4
+ class InitImageResizer():
5
+ """Simple class to create resized copies of an Image while preserving the aspect ratio."""
6
+ def __init__(self,Image):
7
+ self.image = Image
8
+
9
+ def resize(self,width=None,height=None) -> Image:
10
+ """
11
+ Return a copy of the image resized to fit within
12
+ a box width x height. The aspect ratio is
13
+ maintained. If neither width nor height are provided,
14
+ then returns a copy of the original image. If one or the other is
15
+ provided, then the other will be calculated from the
16
+ aspect ratio.
17
+
18
+ Everything is floored to the nearest multiple of 64 so
19
+ that it can be passed to img2img()
20
+ """
21
+ im = self.image
22
+
23
+ ar = im.width/float(im.height)
24
+
25
+ # Infer missing values from aspect ratio
26
+ if not(width or height): # both missing
27
+ width = im.width
28
+ height = im.height
29
+ elif not height: # height missing
30
+ height = int(width/ar)
31
+ elif not width: # width missing
32
+ width = int(height*ar)
33
+
34
+ w_scale = width/im.width
35
+ h_scale = height/im.height
36
+ scale = min(w_scale,h_scale)
37
+ (rw,rh) = (int(scale*im.width),int(scale*im.height))
38
+
39
+ #round everything to multiples of 64
40
+ width,height,rw,rh = map(
41
+ lambda x: x-x%64, (width,height,rw,rh)
42
+ )
43
+
44
+ # no resize necessary, but return a copy
45
+ if im.width == width and im.height == height:
46
+ return im.copy()
47
+
48
+ # otherwise resize the original image so that it fits inside the bounding box
49
+ resized_image = self.image.resize((rw,rh),resample=Image.Resampling.LANCZOS)
50
+ return resized_image
51
+
52
+ def make_grid(image_list, rows=None, cols=None):
53
+ image_cnt = len(image_list)
54
+ if None in (rows, cols):
55
+ rows = floor(sqrt(image_cnt)) # try to make it square
56
+ cols = ceil(image_cnt / rows)
57
+ width = image_list[0].width
58
+ height = image_list[0].height
59
+
60
+ grid_img = Image.new('RGB', (width * cols, height * rows))
61
+ i = 0
62
+ for r in range(0, rows):
63
+ for c in range(0, cols):
64
+ if i >= len(image_list):
65
+ break
66
+ grid_img.paste(image_list[i], (c * width, r * height))
67
+ i = i + 1
68
+
69
+ return grid_img
70
+
ldm/invoke/log.py ADDED
@@ -0,0 +1,66 @@
1
+ """
2
+ Functions for better format logging
3
+ write_log -- logs the name of the output image, prompt, and prompt args to the terminal and different types of file
4
+ 1 write_log_message -- Writes a message to the console
5
+ 2 write_log_files -- Writes a message to files
6
+ 2.1 write_log_default -- File in plain text
7
+ 2.2 write_log_txt -- File in txt format
8
+ 2.3 write_log_markdown -- File in markdown format
9
+ """
10
+
11
+ import os
12
+
13
+
14
+ def write_log(results, log_path, file_types, output_cntr):
15
+ """
16
+ logs the name of the output image, prompt, and prompt args to the terminal and files
17
+ """
18
+ output_cntr = write_log_message(results, output_cntr)
19
+ write_log_files(results, log_path, file_types)
20
+ return output_cntr
21
+
22
+
23
+ def write_log_message(results, output_cntr):
24
+ """logs to the terminal"""
25
+ if len(results) == 0:
26
+ return output_cntr
27
+ log_lines = [f"{path}: {prompt}\n" for path, prompt in results]
28
+ if len(log_lines)>1:
29
+ subcntr = 1
30
+ for l in log_lines:
31
+ print(f"[{output_cntr}.{subcntr}] {l}", end="")
32
+ subcntr += 1
33
+ else:
34
+ print(f"[{output_cntr}] {log_lines[0]}", end="")
35
+ return output_cntr+1
36
+
37
+ def write_log_files(results, log_path, file_types):
38
+ for file_type in file_types:
39
+ if file_type == "txt":
40
+ write_log_txt(log_path, results)
41
+ elif file_type == "md" or file_type == "markdown":
42
+ write_log_markdown(log_path, results)
43
+ else:
44
+ print(f"'{file_type}' format is not supported, so write in plain text")
45
+ write_log_default(log_path, results, file_type)
46
+
47
+
48
+ def write_log_default(log_path, results, file_type):
49
+ plain_txt_lines = [f"{path}: {prompt}\n" for path, prompt in results]
50
+ with open(log_path + "." + file_type, "a", encoding="utf-8") as file:
51
+ file.writelines(plain_txt_lines)
52
+
53
+
54
+ def write_log_txt(log_path, results):
55
+ txt_lines = [f"{path}: {prompt}\n" for path, prompt in results]
56
+ with open(log_path + ".txt", "a", encoding="utf-8") as file:
57
+ file.writelines(txt_lines)
58
+
59
+
60
+ def write_log_markdown(log_path, results):
61
+ md_lines = []
62
+ for path, prompt in results:
63
+ file_name = os.path.basename(path)
64
+ md_lines.append(f"## {file_name}\n![]({file_name})\n\n{prompt}\n")
65
+ with open(log_path + ".md", "a", encoding="utf-8") as file:
66
+ file.writelines(md_lines)