Alexander commited on
Commit
800ac2a
·
1 Parent(s): 27da3b2

save metadata

Browse files
Files changed (1) hide show
  1. app.py +171 -2
app.py CHANGED
@@ -1,9 +1,11 @@
1
  import gc
 
2
  import os
3
  import random
4
  import re
5
  import tempfile
6
  import urllib.request
 
7
  from dataclasses import dataclass
8
  from pathlib import Path
9
  from typing import Iterable, Optional
@@ -65,6 +67,7 @@ else:
65
 
66
  import gradio as gr
67
  from PIL import Image, ImageDraw
 
68
 
69
  # ---------------------------------------------------------------------------
70
  # Constants -- Anima base model
@@ -636,6 +639,132 @@ def _mock_result(image: Image.Image, prompt: str) -> Image.Image:
636
  return result
637
 
638
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  @spaces.GPU(duration=60)
640
  def generate(
641
  prompt: str,
@@ -664,6 +793,7 @@ def generate(
664
  lora3_weight: float,
665
  progress: gr.Progress = gr.Progress(track_tqdm=False),
666
  ):
 
667
  lora_stack = _build_lora_stack(
668
  lora1_key, lora1_url, lora1_weight,
669
  lora2_key, lora2_url, lora2_weight,
@@ -680,6 +810,17 @@ def generate(
680
 
681
  use_controlnet = bool(control_enabled) and control_key not in (None, "Off") and control_image is not None
682
 
 
 
 
 
 
 
 
 
 
 
 
683
  if DEV_MODE:
684
  base = control_image if (use_controlnet and control_image is not None) else Image.new(
685
  "RGB", (width, height), (26, 26, 46)
@@ -690,7 +831,8 @@ def generate(
690
  f"**[DEV MODE]** seed `{seed}` · `{width}×{height}` · "
691
  f"ControlNet: `{control_key if use_controlnet else 'off'}` · LoRA: `{lora_summary}`"
692
  )
693
- return result, seed, f"{result.width}×{result.height}", info
 
694
 
695
  gc.collect()
696
  torch.cuda.empty_cache()
@@ -730,7 +872,8 @@ def generate(
730
  f"**ControlNet-LLLite:** `{control_key + f' (strength {control_strength})' if use_controlnet else 'off'}` \n"
731
  f"**LoRA:** `{lora_summary}`"
732
  )
733
- return image, seed, f"{image.width}×{image.height}", info
 
734
 
735
 
736
  # ---------------------------------------------------------------------------
@@ -1052,6 +1195,17 @@ with gr.Blocks(title="Anima", theme=_theme, css=CSS) as demo:
1052
  result_dims = gr.Textbox(label="Resolution", value="—", interactive=False, elem_classes="meta-tile")
1053
  info_md = gr.Markdown(elem_id="info-box")
1054
 
 
 
 
 
 
 
 
 
 
 
 
1055
  gr.HTML(
1056
  '<footer class="app-footer">Built with Gradio &middot; '
1057
  '<span>circlestone-labs/Anima &middot; kohya-ss/Anima-LLLite</span></footer>'
@@ -1066,6 +1220,21 @@ with gr.Blocks(title="Anima", theme=_theme, css=CSS) as demo:
1066
  lora2_key.change(fn=_slot_url_visible, inputs=[lora2_key], outputs=[lora2_url])
1067
  lora3_key.change(fn=_slot_url_visible, inputs=[lora3_key], outputs=[lora3_url])
1068
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1069
  generate_btn.click(
1070
  fn=generate,
1071
  inputs=[
 
1
  import gc
2
+ import json
3
  import os
4
  import random
5
  import re
6
  import tempfile
7
  import urllib.request
8
+ import uuid
9
  from dataclasses import dataclass
10
  from pathlib import Path
11
  from typing import Iterable, Optional
 
67
 
68
  import gradio as gr
69
  from PIL import Image, ImageDraw
70
+ from PIL.PngImagePlugin import PngInfo
71
 
72
  # ---------------------------------------------------------------------------
73
  # Constants -- Anima base model
 
639
  return result
640
 
641
 
642
+ # ---------------------------------------------------------------------------
643
+ # Generation metadata -- embedded into every output PNG (tEXt chunks) so a
644
+ # past result can be dropped back in and fully reproduced. Returning a
645
+ # filepath (rather than the PIL Image object) from generate() is required:
646
+ # Gradio's Image component passes str/Path returns straight through to disk
647
+ # untouched, but re-encodes a PIL.Image via its own save path, which would
648
+ # silently drop any metadata not explicitly re-attached at save time.
649
+ # ---------------------------------------------------------------------------
650
+ GENERATION_METADATA_KEY = "anima_metadata"
651
+ _OUTPUT_DIR = os.path.join(tempfile.gettempdir(), "anima_outputs")
652
+ os.makedirs(_OUTPUT_DIR, exist_ok=True)
653
+
654
+
655
+ def _build_metadata(
656
+ *, prompt: str, full_prompt: str, negative_prompt: str, positive_prefix: str,
657
+ diffusion_key: str, width: int, height: int, steps: int, cfg_scale: float,
658
+ sigma_shift: Optional[float], seed: int,
659
+ control_enabled: bool, control_key: str, control_strength: float,
660
+ lora1_key: str, lora1_url: str, lora1_weight: float,
661
+ lora2_key: str, lora2_url: str, lora2_weight: float,
662
+ lora3_key: str, lora3_url: str, lora3_weight: float,
663
+ ) -> dict:
664
+ return {
665
+ "app": "Anima", "metadata_version": 1,
666
+ "prompt": prompt, "full_prompt": full_prompt,
667
+ "negative_prompt": negative_prompt, "positive_prefix": positive_prefix,
668
+ "diffusion_key": diffusion_key,
669
+ "width": width, "height": height, "steps": steps, "cfg_scale": cfg_scale,
670
+ "sigma_shift": sigma_shift, "seed": seed,
671
+ "control_enabled": bool(control_enabled), "control_key": control_key,
672
+ "control_strength": control_strength,
673
+ "lora1_key": lora1_key, "lora1_url": lora1_url, "lora1_weight": lora1_weight,
674
+ "lora2_key": lora2_key, "lora2_url": lora2_url, "lora2_weight": lora2_weight,
675
+ "lora3_key": lora3_key, "lora3_url": lora3_url, "lora3_weight": lora3_weight,
676
+ }
677
+
678
+
679
+ def _format_human_readable(meta: dict) -> str:
680
+ """A1111-style block -- purely for humans/other tools eyeballing the PNG info; restore_from_image() reads the JSON blob instead."""
681
+ lines = [meta.get("full_prompt", "")]
682
+ if meta.get("negative_prompt"):
683
+ lines.append(f"Negative prompt: {meta['negative_prompt']}")
684
+
685
+ tail = [
686
+ f"Steps: {meta['steps']}", f"CFG scale: {meta['cfg_scale']}", f"Seed: {meta['seed']}",
687
+ f"Size: {meta['width']}x{meta['height']}", f"Model: {meta['diffusion_key']}",
688
+ ]
689
+ if meta.get("sigma_shift"):
690
+ tail.append(f"Sigma shift: {meta['sigma_shift']}")
691
+ if meta.get("control_enabled"):
692
+ tail.append(f"ControlNet-LLLite: {meta['control_key']} (strength {meta['control_strength']})")
693
+ lora_bits = [
694
+ f"{meta[f'lora{i}_url']} ({meta[f'lora{i}_weight']})" for i in (1, 2, 3)
695
+ if meta.get(f"lora{i}_key") == "Custom URL" and meta.get(f"lora{i}_url")
696
+ ]
697
+ if lora_bits:
698
+ tail.append(f"LoRA: {', '.join(lora_bits)}")
699
+ lines.append(", ".join(tail))
700
+ return "\n".join(lines)
701
+
702
+
703
+ def _save_with_metadata(image: Image.Image, meta: dict) -> str:
704
+ pnginfo = PngInfo()
705
+ pnginfo.add_text(GENERATION_METADATA_KEY, json.dumps(meta, ensure_ascii=False))
706
+ pnginfo.add_text("parameters", _format_human_readable(meta))
707
+ out_path = os.path.join(_OUTPUT_DIR, f"anima_{uuid.uuid4().hex}.png")
708
+ image.save(out_path, format="PNG", pnginfo=pnginfo)
709
+ return out_path
710
+
711
+
712
+ def restore_from_image(file_path):
713
+ """Read back everything generate() embedded and refill the form. The
714
+ control image/mask themselves are never embedded (would bloat every
715
+ output with a second full-size image), so ControlNet has to be
716
+ re-uploaded manually -- everything else reproduces the exact result."""
717
+ if not file_path:
718
+ raise gr.Error("Upload an image first.")
719
+ try:
720
+ with Image.open(file_path) as img:
721
+ raw = img.info.get(GENERATION_METADATA_KEY)
722
+ except Exception as exc:
723
+ raise gr.Error(f"Could not read this image: {exc}")
724
+ if not raw:
725
+ raise gr.Error("No Anima generation data found in this image's metadata.")
726
+ meta = json.loads(raw)
727
+
728
+ control_enabled = bool(meta.get("control_enabled", False))
729
+ control_key = meta.get("control_key", CONTROLNET_CHOICES[1])
730
+ lora1_key = meta.get("lora1_key", "None")
731
+ lora2_key = meta.get("lora2_key", "None")
732
+ lora3_key = meta.get("lora3_key", "None")
733
+
734
+ status = "**Settings restored.**"
735
+ if control_enabled:
736
+ status += " This used ControlNet-LLLite — re-upload the control image manually."
737
+
738
+ return (
739
+ meta.get("prompt", DEFAULT_PROMPT),
740
+ meta.get("negative_prompt", DEFAULT_NEGATIVE),
741
+ meta.get("positive_prefix", DEFAULT_POSITIVE_PREFIX.strip()),
742
+ meta.get("diffusion_key", DEFAULT_DIFFUSION_KEY),
743
+ meta.get("width", 1408),
744
+ meta.get("height", 1408),
745
+ meta.get("steps", 35),
746
+ meta.get("cfg_scale", 6.0),
747
+ meta.get("sigma_shift") or 0.0,
748
+ gr.Number(value=meta.get("seed", -1), interactive=True),
749
+ False, # randomize -> off, we're restoring an explicit seed
750
+ control_enabled,
751
+ control_key,
752
+ gr.Column(visible=control_enabled),
753
+ _controlnet_mask_visible(control_key),
754
+ meta.get("control_strength", 1.0),
755
+ lora1_key,
756
+ gr.Textbox(value=meta.get("lora1_url", ""), visible=(lora1_key == "Custom URL")),
757
+ meta.get("lora1_weight", 1.0),
758
+ lora2_key,
759
+ gr.Textbox(value=meta.get("lora2_url", ""), visible=(lora2_key == "Custom URL")),
760
+ meta.get("lora2_weight", 1.0),
761
+ lora3_key,
762
+ gr.Textbox(value=meta.get("lora3_url", ""), visible=(lora3_key == "Custom URL")),
763
+ meta.get("lora3_weight", 1.0),
764
+ status,
765
+ )
766
+
767
+
768
  @spaces.GPU(duration=60)
769
  def generate(
770
  prompt: str,
 
793
  lora3_weight: float,
794
  progress: gr.Progress = gr.Progress(track_tqdm=False),
795
  ):
796
+ raw_prompt = (prompt or "").strip() or DEFAULT_PROMPT
797
  lora_stack = _build_lora_stack(
798
  lora1_key, lora1_url, lora1_weight,
799
  lora2_key, lora2_url, lora2_weight,
 
810
 
811
  use_controlnet = bool(control_enabled) and control_key not in (None, "Off") and control_image is not None
812
 
813
+ metadata = _build_metadata(
814
+ prompt=raw_prompt, full_prompt=prompt, negative_prompt=negative_prompt,
815
+ positive_prefix=positive_prefix, diffusion_key=diffusion_key,
816
+ width=width, height=height, steps=steps, cfg_scale=cfg_scale,
817
+ sigma_shift=sigma_shift_value, seed=seed,
818
+ control_enabled=use_controlnet, control_key=control_key, control_strength=control_strength,
819
+ lora1_key=lora1_key, lora1_url=lora1_url, lora1_weight=lora1_weight,
820
+ lora2_key=lora2_key, lora2_url=lora2_url, lora2_weight=lora2_weight,
821
+ lora3_key=lora3_key, lora3_url=lora3_url, lora3_weight=lora3_weight,
822
+ )
823
+
824
  if DEV_MODE:
825
  base = control_image if (use_controlnet and control_image is not None) else Image.new(
826
  "RGB", (width, height), (26, 26, 46)
 
831
  f"**[DEV MODE]** seed `{seed}` · `{width}×{height}` · "
832
  f"ControlNet: `{control_key if use_controlnet else 'off'}` · LoRA: `{lora_summary}`"
833
  )
834
+ out_path = _save_with_metadata(result, metadata)
835
+ return out_path, seed, f"{result.width}×{result.height}", info
836
 
837
  gc.collect()
838
  torch.cuda.empty_cache()
 
872
  f"**ControlNet-LLLite:** `{control_key + f' (strength {control_strength})' if use_controlnet else 'off'}` \n"
873
  f"**LoRA:** `{lora_summary}`"
874
  )
875
+ out_path = _save_with_metadata(image, metadata)
876
+ return out_path, seed, f"{image.width}×{image.height}", info
877
 
878
 
879
  # ---------------------------------------------------------------------------
 
1195
  result_dims = gr.Textbox(label="Resolution", value="—", interactive=False, elem_classes="meta-tile")
1196
  info_md = gr.Markdown(elem_id="info-box")
1197
 
1198
+ with gr.Column(elem_classes="q-card"):
1199
+ gr.HTML('<div class="card-label">Restore settings</div>')
1200
+ gr.HTML(
1201
+ '<p class="card-note">Every image Anima generates has its full settings '
1202
+ '(prompt, seed, size, model, LoRA…) embedded in its PNG data. Drop one back in '
1203
+ 'to restore them — <b>ControlNet control image excepted</b>, re-upload that '
1204
+ 'yourself.</p>'
1205
+ )
1206
+ restore_upload = gr.Image(type="filepath", show_label=False, height=110, elem_id="restore-drop")
1207
+ restore_status = gr.Markdown()
1208
+
1209
  gr.HTML(
1210
  '<footer class="app-footer">Built with Gradio &middot; '
1211
  '<span>circlestone-labs/Anima &middot; kohya-ss/Anima-LLLite</span></footer>'
 
1220
  lora2_key.change(fn=_slot_url_visible, inputs=[lora2_key], outputs=[lora2_url])
1221
  lora3_key.change(fn=_slot_url_visible, inputs=[lora3_key], outputs=[lora3_url])
1222
 
1223
+ restore_upload.upload(
1224
+ fn=restore_from_image,
1225
+ inputs=[restore_upload],
1226
+ outputs=[
1227
+ prompt, negative_prompt, positive_prefix,
1228
+ diffusion_key,
1229
+ width, height, steps, cfg_scale, sigma_shift, seed, randomize,
1230
+ control_enabled, control_key, controlnet_panel, control_mask, control_strength,
1231
+ lora1_key, lora1_url, lora1_weight,
1232
+ lora2_key, lora2_url, lora2_weight,
1233
+ lora3_key, lora3_url, lora3_weight,
1234
+ restore_status,
1235
+ ],
1236
+ )
1237
+
1238
  generate_btn.click(
1239
  fn=generate,
1240
  inputs=[