beercan commited on
Commit
7e11b87
·
verified ·
1 Parent(s): d7588d9

Upload stability.livemd

Browse files
Files changed (1) hide show
  1. public-apps/stability.livemd +696 -0
public-apps/stability.livemd ADDED
@@ -0,0 +1,696 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- livebook:{"app_settings":{"access_type":"public","auto_shutdown_ms":3600000,"multi_session":true,"show_existing_sessions":false,"slug":"stability"},"autosave_interval_s":null,"file_entries":[{"name":"giraffe-explosion-2.webp","type":"attachment"}]} -->
2
+
3
+ # Stability AI
4
+
5
+ ```elixir
6
+ Mix.install([
7
+ {:tesla, "~> 1.4"},
8
+
9
+ # optional, but recommended adapter
10
+ {:hackney, "~> 1.17"},
11
+
12
+ # optional, required by JSON middleware
13
+ {:jason, ">= 1.0.0"},
14
+ {:kino, "~> 0.12.0"}
15
+ ])
16
+
17
+ Kino.nothing()
18
+ ```
19
+
20
+ ## Generate images with Stable Diffusion
21
+
22
+ ```elixir
23
+ Kino.Markdown.new("""
24
+ Stability AI provides their Stability Diffusion family of image generation models. This application allows you to interface with Stability AI's API:s to generate images with your choice of models.
25
+
26
+ *To be able to generate images you need your Stability AI API-key. When you provide the key it will be used with every request to Stability AI but otherwise will be private to your session - meaning you will be asked again for your API-key if you were to close this session or if it were timed out.*
27
+
28
+ """)
29
+ ```
30
+
31
+ ```elixir
32
+ Kino.FS.file_path("giraffe-explosion-2.webp")
33
+ |> File.read!()
34
+ |> Kino.Image.new("image/webp")
35
+ |> Kino.render()
36
+
37
+ Kino.Markdown.new(
38
+ "> Example of image generated with Stable Diffusion 3: `A running giraffe, behind it an explosion, all surrounded by the vastness of space and a galaxy.` "
39
+ )
40
+ ```
41
+
42
+ ```elixir
43
+ defmodule StabilityAI do
44
+ use Tesla
45
+
46
+ @sd3_uri "/v2beta/stable-image/generate/sd3"
47
+ @models %{
48
+ sd1_6: "stable-diffusion-v1-6",
49
+ sdxl: "stable-diffusion-xl-1024-v1-0",
50
+ sd3: "sd3",
51
+ sd3_turbo: "sd3-turbo"
52
+ }
53
+
54
+ @sd_uris %{
55
+ sd1_6: "/v1/generation/stable-diffusion-v1-6/text-to-image",
56
+ sdxl: "/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image",
57
+ sd3: "/v2beta/stable-image/generate/sd3",
58
+ sd3_turbo: "/v2beta/stable-image/generate/sd3"
59
+ }
60
+
61
+ @base_url "https://api.stability.ai"
62
+
63
+ def image_path() do
64
+ Kino.tmp_dir() <> "/image_generations/"
65
+ end
66
+
67
+ def get_image(model_prompt, model, api_key) do
68
+ case model do
69
+ :sd1_6 -> get_image_sd1(model_prompt, model, api_key)
70
+ :sdxl -> get_image_sd1(model_prompt, model, api_key)
71
+ :sd3 -> get_image_sd3(model_prompt, model, api_key)
72
+ :sd3_turbo -> get_image_sd3(model_prompt, model, api_key)
73
+ end
74
+ end
75
+
76
+ defp get_image_sd1(
77
+ %{
78
+ prompt: prompt,
79
+ negative_prompt: _negative_prompt,
80
+ aspect_ratio: aspect_ratio,
81
+ style_preset: style_preset
82
+ },
83
+ model,
84
+ api_key
85
+ )
86
+ when byte_size(prompt) > 5 and byte_size(aspect_ratio) >= 3 do
87
+ client =
88
+ Tesla.client([
89
+ {Tesla.Middleware.BaseUrl, @base_url},
90
+ Tesla.Middleware.JSON,
91
+ {Tesla.Middleware.Headers,
92
+ [
93
+ {"Authorization", "Bearer #{api_key}"},
94
+ {"Accept", "image/png"},
95
+ {"Stability-Client-ID", "Livebook Stability"},
96
+ {"Stability-Client-Version", "0.1"}
97
+ ]}
98
+ ])
99
+
100
+ uri = Map.get(@sd_uris, model)
101
+
102
+ {width, height} =
103
+ case aspect_ratio do
104
+ "21:9" -> {1536, 640}
105
+ "16:9" -> {1344, 768}
106
+ "3:2" -> {1216, 832}
107
+ "1:1" -> {1024, 1024}
108
+ "2:3" -> {832, 1216}
109
+ "9:16" -> {768, 1344}
110
+ "9:21" -> {640, 1536}
111
+ end
112
+
113
+ body = %{
114
+ text_prompts: [%{text: prompt, weight: 0.5}],
115
+ height: height,
116
+ width: width,
117
+ cfg_scale: 7,
118
+ clip_guidance_preset: "NONE",
119
+ samples: 1,
120
+ seed: 0,
121
+ steps: 30
122
+ }
123
+
124
+ body =
125
+ case style_preset do
126
+ nil -> body
127
+ _ -> Map.put(body, :style_preset, style_preset)
128
+ end
129
+
130
+ post(
131
+ client,
132
+ uri,
133
+ body
134
+ )
135
+ end
136
+
137
+ defp get_image_sd3(
138
+ %{
139
+ prompt: prompt,
140
+ negative_prompt: negative_prompt,
141
+ aspect_ratio: aspect_ratio
142
+ },
143
+ model,
144
+ api_key
145
+ )
146
+ when byte_size(prompt) > 5 and byte_size(aspect_ratio) >= 3 do
147
+ client =
148
+ Tesla.client([
149
+ {Tesla.Middleware.BaseUrl, @base_url},
150
+ Tesla.Middleware.FormUrlencoded
151
+ ])
152
+
153
+ body_template =
154
+ Tesla.Multipart.new()
155
+ |> Tesla.Multipart.add_field("prompt", prompt)
156
+ |> Tesla.Multipart.add_field("output_format", "png")
157
+ |> Tesla.Multipart.add_field("aspect_ratio", aspect_ratio)
158
+ |> Tesla.Multipart.add_field("model", Map.get(@models, model))
159
+
160
+ body =
161
+ case byte_size(negative_prompt) do
162
+ 0 -> body_template
163
+ _ -> body_template |> Tesla.Multipart.add_field("negative_prompt", negative_prompt)
164
+ end
165
+
166
+ post(
167
+ client,
168
+ @sd3_uri,
169
+ body,
170
+ headers: [
171
+ {"Authorization", "Bearer #{api_key}"},
172
+ {"Accept", "image/*"}
173
+ ]
174
+ )
175
+ end
176
+
177
+ def get_balance(api_key) do
178
+ client =
179
+ Tesla.client([
180
+ {Tesla.Middleware.BaseUrl, @base_url},
181
+ Tesla.Middleware.JSON,
182
+ {Tesla.Middleware.Headers,
183
+ [
184
+ {"Authorization", "Bearer #{api_key}"},
185
+ {"Accept", "application/json"},
186
+ {"Stability-Client-ID", "Livebook Stability"},
187
+ {"Stability-Client-Version", "0.1"}
188
+ ]}
189
+ ])
190
+
191
+ get(
192
+ client,
193
+ "/v1/user/balance"
194
+ )
195
+ end
196
+
197
+ def throttled_submission(form, data, status_frame: stability_status) do
198
+ case RateLimitedForm.rate_limit(form) do
199
+ :ok ->
200
+ IO.puts("Generating with Stability AI...")
201
+ Kino.Frame.render(stability_status, Kino.Markdown.new("Loading..."))
202
+
203
+ StabilityAI.get_image(data, ModelVault.value(), KeyVault.value())
204
+
205
+ :halt ->
206
+ IO.puts("Please wait a few seconds before submitting again")
207
+ nil
208
+ end
209
+ end
210
+
211
+ def handle_response(
212
+ response,
213
+ data,
214
+ status_frame: stability_status,
215
+ preview_frame: preview_frame,
216
+ output_frame: output_frame,
217
+ balance_frame: balance_frame
218
+ ) do
219
+ case response do
220
+ {:ok, %Tesla.Env{status: 200, body: body}} ->
221
+ filename_template =
222
+ :crypto.hash(:sha, "#{data.prompt} - #{:os.system_time(:second)}")
223
+ |> Base.encode16(case: :lower)
224
+
225
+ filename = filename_template <> ".png"
226
+
227
+ unless File.exists?(image_path()) do
228
+ File.mkdir!(image_path())
229
+ end
230
+
231
+ filepath = image_path() <> filename
232
+
233
+ File.write!(filepath, body)
234
+ IO.puts("Written file to '#{filepath}'")
235
+
236
+ content = File.read!(filepath)
237
+ image = Kino.Image.new(content, "image/png")
238
+
239
+ ImagesVault.store(filepath)
240
+
241
+ Kino.Frame.render(stability_status, Kino.Markdown.new("Done!"))
242
+
243
+ Kino.Frame.render(preview_frame, image)
244
+
245
+ images =
246
+ for image_path <- ImagesVault.value() do
247
+ File.read!(image_path) |> Kino.Image.new(:png)
248
+ end
249
+
250
+ images_grid = Kino.Layout.grid(images, columns: 3)
251
+
252
+ Kino.Frame.render(output_frame, images_grid)
253
+
254
+ case StabilityAI.get_balance(KeyVault.value()) do
255
+ {:ok, %Tesla.Env{status: 200, body: body}} ->
256
+ %{"credits" => credits} = body
257
+ Kino.Frame.render(balance_frame, Kino.Markdown.new("**Balance** `#{credits}`"))
258
+
259
+ _ ->
260
+ :error
261
+ end
262
+
263
+ {:ok, filepath}
264
+
265
+ _ ->
266
+ Kino.Frame.render(stability_status, Kino.Markdown.new("Something went wrong!"))
267
+ IO.inspect(response)
268
+ {:error, response}
269
+ end
270
+ end
271
+ end
272
+
273
+ defmodule RateLimitedForm do
274
+ use GenServer
275
+
276
+ def start_link(form, opts) do
277
+ GenServer.start_link(__MODULE__, {form, opts}, name: __MODULE__)
278
+ end
279
+
280
+ def init({form, _opts}) do
281
+ {:ok, %{form: form, last_submitted: nil}}
282
+ end
283
+
284
+ def rate_limit(form) do
285
+ GenServer.call(__MODULE__, {:rate_limit, form})
286
+ end
287
+
288
+ def handle_call({:rate_limit, _form}, _from, state) do
289
+ current_time = :os.system_time(:second)
290
+
291
+ case state.last_submitted && current_time - state.last_submitted < 5 do
292
+ true ->
293
+ {:reply, :halt, state}
294
+
295
+ _ ->
296
+ state = %{state | last_submitted: current_time}
297
+ {:reply, :ok, state}
298
+ end
299
+ end
300
+ end
301
+
302
+ defmodule KeyVault do
303
+ use Agent
304
+
305
+ def start_link(initial_value) do
306
+ Agent.start_link(fn -> initial_value end, name: __MODULE__)
307
+ end
308
+
309
+ def value do
310
+ Agent.get(__MODULE__, & &1)
311
+ end
312
+
313
+ def store(value) do
314
+ Agent.update(__MODULE__, fn _ -> value end)
315
+ end
316
+ end
317
+
318
+ defmodule ImagesVault do
319
+ use Agent
320
+
321
+ def start_link(initial_value) do
322
+ Agent.start_link(fn -> initial_value end, name: __MODULE__)
323
+ end
324
+
325
+ def value do
326
+ Agent.get(__MODULE__, & &1)
327
+ end
328
+
329
+ def latest do
330
+ Agent.get(__MODULE__, fn images ->
331
+ List.first(images)
332
+ end)
333
+ end
334
+
335
+ def store(image) do
336
+ Agent.update(__MODULE__, fn images -> [image | images] end)
337
+ end
338
+ end
339
+
340
+ defmodule ModelVault do
341
+ use Agent
342
+
343
+ def start_link(initial_value) do
344
+ Agent.start_link(fn -> initial_value end, name: __MODULE__)
345
+ end
346
+
347
+ def value do
348
+ Agent.get(__MODULE__, & &1)
349
+ end
350
+
351
+ def store(value) do
352
+ Agent.update(__MODULE__, fn _ -> value end)
353
+ end
354
+ end
355
+
356
+ KeyVault.start_link("")
357
+ ImagesVault.start_link([])
358
+ ModelVault.start_link(:sd1_6)
359
+
360
+ balance_frame = Kino.Frame.new()
361
+
362
+ api_key_form =
363
+ Kino.Control.form(
364
+ [
365
+ api_key: Kino.Input.password("Stability API Key")
366
+ ],
367
+ submit: "OK"
368
+ )
369
+
370
+ api_key_status = Kino.Frame.new(placeholder: false)
371
+
372
+ Kino.listen(api_key_form, fn event ->
373
+ case event do
374
+ %{type: :submit} ->
375
+ %{data: %{api_key: api_key}} = event
376
+
377
+ KeyVault.store(api_key)
378
+ Kino.Frame.append(api_key_status, Kino.Markdown.new("*Updated key*"))
379
+
380
+ case StabilityAI.get_balance(KeyVault.value()) do
381
+ {:ok, %Tesla.Env{status: 200, body: body}} ->
382
+ %{"credits" => credits} = body
383
+ Kino.Frame.render(balance_frame, Kino.Markdown.new("**Balance** `#{credits}`"))
384
+
385
+ _ ->
386
+ :error
387
+ end
388
+
389
+ _ ->
390
+ IO.puts("Error: Not specified event for API Key form!")
391
+ end
392
+ end)
393
+
394
+ basic_form_fields = [
395
+ prompt: Kino.Input.textarea("Prompt"),
396
+ negative_prompt: Kino.Input.text("Negative Prompt"),
397
+ aspect_ratio:
398
+ Kino.Input.select("Aspect Ratio", [
399
+ {"21:9", "21:9"},
400
+ {"16:9", "16:9"},
401
+ {"3:2", "3:2"},
402
+ {"1:1", "1:1"},
403
+ {"2:3", "2:3"},
404
+ {"9:16", "9:16"},
405
+ {"9:21", "9:21"}
406
+ ])
407
+ ]
408
+
409
+ v1_form =
410
+ Kino.Control.form(
411
+ basic_form_fields ++
412
+ [
413
+ style_preset:
414
+ Kino.Input.select("Predefined Style", [
415
+ {"3d-model", "3D Model"},
416
+ {"analog-film", "Analog Film"},
417
+ {"anime", "Anime"},
418
+ {"cinematic", "Cinematic"},
419
+ {"comic-book", "Comic Book"},
420
+ {"digital-art", "Digital Art"},
421
+ {"enhance", "Enhance"},
422
+ {"fantasy-art", "Fantasy Art"},
423
+ {"isometric", "Isometric"},
424
+ {"line-art", "Line Art"},
425
+ {"low-poly", "Low Poly"},
426
+ {"modeling-compound", "Modeling Compound"},
427
+ {"neon-punk", "Neon Punk"},
428
+ {"origami", "Origami"},
429
+ {"photographic", "Photographic"},
430
+ {"pixel-art", "Pixel Art"},
431
+ {"tile-texture", "Tile Texture"},
432
+ {nil, "None"}
433
+ ])
434
+ ],
435
+ submit: "Submit"
436
+ )
437
+
438
+ v3_form =
439
+ Kino.Control.form(
440
+ basic_form_fields,
441
+ submit: "Submit"
442
+ )
443
+
444
+ prompt_frame = Kino.Frame.new()
445
+ Kino.Frame.render(prompt_frame, v1_form)
446
+
447
+ case RateLimitedForm.start_link(v1_form, []) do
448
+ {:error, {:already_started, _pid}} -> :ok
449
+ {:ok, _pid} -> :ok
450
+ x -> x
451
+ end
452
+
453
+ case RateLimitedForm.start_link(v3_form, []) do
454
+ {:error, {:already_started, _pid}} -> :ok
455
+ {:ok, _pid} -> :ok
456
+ x -> x
457
+ end
458
+
459
+ Kino.nothing()
460
+ ```
461
+
462
+ ```elixir
463
+ Kino.Layout.grid(
464
+ [
465
+ api_key_form,
466
+ api_key_status
467
+ ],
468
+ columns: 2
469
+ )
470
+ ```
471
+
472
+ ```elixir
473
+ # Select model to use
474
+ # model_form =
475
+ # Kino.Control.form(
476
+ # [
477
+ # model:
478
+ # Kino.Input.select("Model", [
479
+ # {:sd1_6, "SD 1.6"},
480
+ # {:sdxl, "SDXL"},
481
+ # {:sd3, "SD 3"},
482
+ # {:sd3_turbo, "SD 3-Turbo"}
483
+ # ])
484
+ # ],
485
+ # submit: "Select Model"
486
+ # )
487
+
488
+ model_selection =
489
+ Kino.Input.select("Model", [
490
+ {:sd1_6, "SD 1.6"},
491
+ {:sdxl, "SDXL"},
492
+ {:sd3, "SD 3"},
493
+ {:sd3_turbo, "SD 3-Turbo"}
494
+ ])
495
+
496
+ Kino.listen(model_selection, fn event ->
497
+ %{value: model} = event
498
+ ModelVault.store(model)
499
+
500
+ case model do
501
+ :sd1_6 -> Kino.Frame.render(prompt_frame, v1_form)
502
+ :sdxl -> Kino.Frame.render(prompt_frame, v1_form)
503
+ :sd3 -> Kino.Frame.render(prompt_frame, v3_form)
504
+ :sd3_turbo -> Kino.Frame.render(prompt_frame, v3_form)
505
+ end
506
+ end)
507
+
508
+ model_selection
509
+ ```
510
+
511
+ ````elixir
512
+ stability_status = Kino.Frame.new()
513
+ prompt_status = Kino.Frame.new()
514
+
515
+ preview_frame = Kino.Frame.new(placeholder: false)
516
+ latest_image_file = ImagesVault.latest()
517
+
518
+ case latest_image_file do
519
+ nil ->
520
+ nil
521
+
522
+ _ ->
523
+ latest_image = File.read!(latest_image_file) |> Kino.Image.new(:png)
524
+ Kino.Frame.render(preview_frame, latest_image)
525
+ end
526
+
527
+ status_layout =
528
+ Kino.Layout.grid([Kino.Markdown.new("### *Status*"), prompt_status, stability_status],
529
+ columns: 1
530
+ )
531
+
532
+ form_frame = Kino.Frame.new()
533
+ form_layout = Kino.Layout.grid([form_frame, status_layout], columns: 2)
534
+
535
+ Kino.render(form_layout)
536
+
537
+ Kino.Frame.append(form_frame, Kino.Markdown.new("## Generate Image"))
538
+ Kino.Frame.append(form_frame, balance_frame)
539
+ Kino.Frame.append(form_frame, prompt_frame)
540
+
541
+ images =
542
+ for image_path <- ImagesVault.value() do
543
+ File.read!(image_path) |> Kino.Image.new(:png)
544
+ end
545
+
546
+ images_grid = Kino.Layout.grid(images, columns: 3)
547
+ output_frame = Kino.Frame.new()
548
+
549
+ Kino.Markdown.new("""
550
+
551
+ ___
552
+
553
+ Preview:
554
+
555
+ """)
556
+ |> Kino.render()
557
+
558
+ Kino.render(preview_frame)
559
+ Kino.Frame.append(output_frame, images_grid)
560
+
561
+ Kino.listen(v1_form, fn event ->
562
+ case event do
563
+ %{type: :submit} ->
564
+ %{data: data} = event
565
+
566
+ model = ModelVault.value()
567
+
568
+ Kino.Frame.render(
569
+ prompt_status,
570
+ Kino.Markdown.new("""
571
+ ```json
572
+ {
573
+ "prompt": #{data.prompt}
574
+ "negative_prompt": #{data.negative_prompt}
575
+ "aspect_ratio": #{data.aspect_ratio}
576
+ "model": #{model}
577
+ }
578
+ ```
579
+ """)
580
+ )
581
+
582
+ response = StabilityAI.throttled_submission(v1_form, data, status_frame: stability_status)
583
+
584
+ StabilityAI.handle_response(
585
+ response,
586
+ data,
587
+ status_frame: stability_status,
588
+ preview_frame: preview_frame,
589
+ output_frame: output_frame,
590
+ balance_frame: balance_frame
591
+ )
592
+
593
+ _ ->
594
+ IO.puts("What?")
595
+ end
596
+ end)
597
+
598
+ Kino.listen(v3_form, fn event ->
599
+ case event do
600
+ %{type: :submit} ->
601
+ %{data: data} = event
602
+
603
+ model = ModelVault.value()
604
+
605
+ Kino.Frame.render(
606
+ prompt_status,
607
+ Kino.Markdown.new("""
608
+ ```json
609
+ {
610
+ "prompt": #{data.prompt}
611
+ "negative_prompt": #{data.negative_prompt}
612
+ "aspect_ratio": #{data.aspect_ratio}
613
+ "model": #{model}
614
+ }
615
+ ```
616
+ """)
617
+ )
618
+
619
+ response = StabilityAI.throttled_submission(v1_form, data, status_frame: stability_status)
620
+
621
+ StabilityAI.handle_response(
622
+ response,
623
+ data,
624
+ status_frame: stability_status,
625
+ preview_frame: preview_frame,
626
+ output_frame: output_frame,
627
+ balance_frame: balance_frame
628
+ )
629
+
630
+ _ ->
631
+ IO.puts("What?")
632
+ end
633
+ end)
634
+
635
+ Kino.Markdown.new("""
636
+
637
+ ___
638
+
639
+
640
+
641
+ """)
642
+ |> Kino.render()
643
+
644
+ output_frame
645
+ ````
646
+
647
+ ```elixir
648
+ download_button = Kino.Control.button("ZIP images")
649
+ download_frame = Kino.Frame.new()
650
+ Kino.render(download_button)
651
+ Kino.render(download_frame)
652
+
653
+ Kino.listen(download_button, fn _event ->
654
+ now =
655
+ DateTime.utc_now()
656
+ |> DateTime.to_string()
657
+ |> String.replace(~r/[:\s-]/, "")
658
+ |> String.replace(".", "_")
659
+
660
+ image_files = ImagesVault.value()
661
+
662
+ files_to_zip =
663
+ image_files
664
+ |> Enum.map(fn file_path ->
665
+ String.replace(file_path, StabilityAI.image_path(), "")
666
+ |> String.to_charlist()
667
+ end)
668
+
669
+ zip_filename = "stability_images_#{now}.zip"
670
+ zip_filepath = StabilityAI.image_path() <> zip_filename
671
+
672
+ case :zip.create(
673
+ String.to_charlist(zip_filepath),
674
+ files_to_zip,
675
+ cwd: String.to_charlist(StabilityAI.image_path())
676
+ ) do
677
+ {:ok, _} ->
678
+ download_prompt =
679
+ Kino.Download.new(
680
+ fn -> "#{zip_filepath}" |> File.read!() end,
681
+ filename: zip_filename,
682
+ label: zip_filename
683
+ )
684
+
685
+ IO.puts("ZIP file created: #{zip_filename}")
686
+
687
+ Kino.Frame.render(download_frame, download_prompt)
688
+
689
+ x ->
690
+ IO.inspect(x)
691
+ IO.puts("Error creating ZIP")
692
+ end
693
+ end)
694
+
695
+ Kino.nothing()
696
+ ```