verymehari commited on
Commit
5c7bd9a
·
verified ·
1 Parent(s): bde0527

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,13 +1,48 @@
1
  ---
2
  title: Visual Lineage
3
- emoji: 📚
4
- colorFrom: yellow
5
  colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.18.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Visual Lineage
3
+ emoji: 🧬
4
+ colorFrom: green
5
  colorTo: yellow
6
  sdk: gradio
7
+ sdk_version: 5.0.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: LoRA provenance for imagined instruments.
12
  ---
13
 
14
+ # Visual Lineage
15
+
16
+ *A 23andMe for imagined instruments.*
17
+
18
+ Visual Lineage trains culturally specific LoRAs for niche instruments that general image models barely know, then traces every generated image back through its visual ancestry.
19
+
20
+ This hackathon prototype starts with:
21
+
22
+ - **LoRA:** [`eritrean_krar_v1`](https://huggingface.co/build-small-hackathon/visual-lineage-eritrean_krar_v1)
23
+ - **Subject:** Eritrean krar / Horn of Africa bowl lyre
24
+ - **Base inference model:** `black-forest-labs/FLUX.2-klein-4B`
25
+ - **Training target:** `black-forest-labs/FLUX.2-klein-base-4B`
26
+ - **Dataset:** 38 openly licensed images, with source/license manifest in the LoRA repo
27
+
28
+ ## What the app does
29
+
30
+ 1. Select one or more LoRA lineage roots.
31
+ 2. Generate an imagined instrument image with FLUX.2 klein.
32
+ 3. Show the ancestry breakdown, trigger words, Hugging Face source links, and raw provenance JSON.
33
+
34
+ ## Model size / Build Small compliance
35
+
36
+ Every model used is below the 32B parameter cap. The live image model is FLUX.2 [klein] 4B.
37
+
38
+ ## Notes
39
+
40
+ Live generation requires GPU and Hugging Face access to the FLUX.2 klein model. If the Space is asleep/cold, the first generation can take a while.
41
+
42
+ ## Demo video
43
+
44
+ TODO: add demo video link.
45
+
46
+ ## Social post
47
+
48
+ TODO: add social post link.
app.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import html
4
+ import json
5
+ import sys
6
+ import traceback
7
+ from pathlib import Path
8
+
9
+ import gradio as gr
10
+
11
+ ROOT = Path(__file__).resolve().parent
12
+ sys.path.insert(0, str(ROOT))
13
+
14
+ from compose.merge import compose
15
+ from compose.provenance import provenance_sentence
16
+ from registry import lora_choices
17
+
18
+ SAGE = "#5f6f52"
19
+ CREAM = "#f6f1e8"
20
+ TERRACOTTA = "#b7653c"
21
+
22
+
23
+ def render_node(node: dict, depth: int = 0) -> str:
24
+ margin = depth * 22
25
+ children = "".join(render_node(child, depth + 1) for child in node.get("children", []))
26
+ status = node.get("status", "unknown")
27
+ checkpoint = node.get("checkpoint_step") or "not selected yet"
28
+ cycle = "<div class='warn'>Cycle detected — recursion stopped.</div>" if node.get("cycle_detected") else ""
29
+ return f"""
30
+ <div class="lineage-card" style="margin-left:{margin}px">
31
+ <div class="card-top">
32
+ <strong>{html.escape(node.get('cultural_source') or node['lora_id'])}</strong>
33
+ <span class="pill">{node['weight_pct']:g}%</span>
34
+ </div>
35
+ <div class="muted">{html.escape(node['lora_id'])} · {html.escape(str(node.get('type')))} · {html.escape(str(status))}</div>
36
+ <div class="muted">checkpoint: {html.escape(str(checkpoint))}</div>
37
+ <a href="https://huggingface.co/{html.escape(node.get('hf_repo') or '')}" target="_blank">{html.escape(node.get('hf_repo') or '')}</a>
38
+ {cycle}
39
+ </div>
40
+ {children}
41
+ """
42
+
43
+
44
+ def render_lineage(provenance: dict) -> str:
45
+ ancestry = provenance.get("ancestry", [])
46
+ bars = []
47
+ colors = [SAGE, TERRACOTTA, "#3f5f8f", "#9a7b33", "#72517e"]
48
+ for i, node in enumerate(ancestry):
49
+ bars.append(
50
+ f"<div title='{html.escape(node['lora_id'])}' style='width:{node['weight_pct']}%;background:{colors[i % len(colors)]}'>{node['weight_pct']:g}%</div>"
51
+ )
52
+ cards = "".join(render_node(node) for node in ancestry)
53
+ raw = html.escape(json.dumps(provenance, indent=2))
54
+ sentence = html.escape(provenance_sentence(provenance))
55
+ full_prompt = html.escape(provenance.get("full_prompt", ""))
56
+ model = html.escape(provenance.get("inference_model", provenance.get("base_model", "")))
57
+ return f"""
58
+ <style>
59
+ .vl-wrap {{ background:{CREAM}; border:1px solid #d9cfbd; border-radius:18px; padding:18px; color:#263322; }}
60
+ .stack {{ display:flex; height:38px; overflow:hidden; border-radius:999px; box-shadow: inset 0 0 0 1px rgba(0,0,0,.12); margin:12px 0 18px; }}
61
+ .stack div {{ color:white; font-weight:700; display:flex; align-items:center; justify-content:center; min-width:48px; }}
62
+ .lineage-card {{ background:white; border:1px solid #ddd2c0; border-radius:14px; padding:12px; margin-top:10px; }}
63
+ .card-top {{ display:flex; justify-content:space-between; gap:12px; }}
64
+ .pill {{ background:{SAGE}; color:white; padding:3px 10px; border-radius:999px; font-size:12px; }}
65
+ .muted {{ color:#66705f; font-size:13px; margin-top:3px; }}
66
+ .warn {{ color:#9b341f; font-weight:700; margin-top:6px; }}
67
+ details {{ margin-top:16px; }}
68
+ pre {{ white-space:pre-wrap; font-size:12px; background:#fff; padding:12px; border-radius:10px; }}
69
+ </style>
70
+ <div class="vl-wrap">
71
+ <h3>Visual lineage</h3>
72
+ <p>{sentence}</p>
73
+ <div class="muted">Model: {model}</div>
74
+ <div class="muted">Full prompt: {full_prompt}</div>
75
+ <div class="stack">{''.join(bars)}</div>
76
+ {cards}
77
+ <details><summary>Raw provenance JSON</summary><pre>{raw}</pre></details>
78
+ </div>
79
+ """
80
+
81
+
82
+ def generate(lora_a: str, lora_b: str, blend_a: int, prompt: str, seed: int, size: int):
83
+ if not prompt.strip():
84
+ raise gr.Error("Give the image a prompt first.")
85
+ weight_a = blend_a / 100
86
+ weight_b = 1 - weight_a
87
+ try:
88
+ result = compose(
89
+ lora_ids=[lora_a, lora_b],
90
+ weights=[weight_a, weight_b],
91
+ prompt=prompt,
92
+ seed=int(seed),
93
+ registry_path=str(ROOT / "registry/loras.json"),
94
+ output_dir=str(ROOT / "outputs"),
95
+ width=int(size),
96
+ height=int(size),
97
+ )
98
+ return result["image"], render_lineage(result["provenance"])
99
+ except Exception as e:
100
+ traceback.print_exc()
101
+ raise gr.Error(f"Generation failed: {type(e).__name__}: {e}")
102
+
103
+
104
+ def build_demo():
105
+ choices = lora_choices(ROOT / "registry/loras.json")
106
+ default_a = "eritrean_krar_v1" if any(v == "eritrean_krar_v1" for _, v in choices) else choices[0][1]
107
+ default_b = default_a
108
+ with gr.Blocks(title="Visual Lineage", theme=gr.themes.Soft(primary_hue="green")) as demo:
109
+ gr.Markdown(
110
+ "# Visual Lineage\n"
111
+ "*A 23andMe for imagined instruments.*\n\n"
112
+ "Live FLUX.2 [klein] inference with published LoRA provenance. "
113
+ "The first root node is `eritrean_krar_v1`, trained from openly licensed krar / bowl-lyre imagery."
114
+ )
115
+ with gr.Row():
116
+ with gr.Column(scale=1):
117
+ lora_a = gr.Dropdown(label="Instrument/source A", choices=choices, value=default_a)
118
+ lora_b = gr.Dropdown(label="Instrument/source B", choices=choices, value=default_b)
119
+ blend = gr.Slider(0, 100, value=100, step=1, label="Blend: A % / B %")
120
+ prompt = gr.Textbox(label="Describe the imagined instrument image", value="a musician holding a newly invented bowl-shaped string instrument in a small warm room", lines=3)
121
+ seed = gr.Number(label="Seed", value=42, precision=0)
122
+ size = gr.Dropdown(label="Image size", choices=[512, 768, 1024], value=768)
123
+ btn = gr.Button("Generate with live LoRA", variant="primary")
124
+ with gr.Column(scale=2):
125
+ output_image = gr.Image(label="Generated image", type="pil")
126
+ lineage_panel = gr.HTML(label="Visual lineage")
127
+ btn.click(generate, [lora_a, lora_b, blend, prompt, seed, size], [output_image, lineage_panel], show_progress="full")
128
+ return demo
129
+
130
+
131
+ if __name__ == "__main__":
132
+ build_demo().launch()
compose/__init__.py ADDED
File without changes
compose/merge.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from compose.provenance import build_provenance, load_registry, lora_map, normalize_weights
7
+
8
+ BASE_INFERENCE_MODEL = "black-forest-labs/FLUX.2-klein-4B"
9
+
10
+ _PIPE = None
11
+ _LOADED_ADAPTERS: set[str] = set()
12
+
13
+
14
+ def _device():
15
+ import torch
16
+
17
+ if torch.cuda.is_available():
18
+ return "cuda", torch.bfloat16
19
+ if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
20
+ return "mps", torch.float16
21
+ return "cpu", torch.float32
22
+
23
+
24
+ def _load_pipe():
25
+ global _PIPE
26
+ if _PIPE is not None:
27
+ return _PIPE
28
+
29
+ import torch
30
+ from diffusers import Flux2KleinPipeline
31
+
32
+ device, dtype = _device()
33
+ pipe = Flux2KleinPipeline.from_pretrained(
34
+ BASE_INFERENCE_MODEL,
35
+ torch_dtype=dtype,
36
+ token=None,
37
+ )
38
+ pipe = pipe.to(device)
39
+ _PIPE = pipe
40
+ return pipe
41
+
42
+
43
+ def _load_adapters(pipe, lora_ids: list[str], registry_by_id: dict[str, dict[str, Any]]) -> None:
44
+ for lora_id in lora_ids:
45
+ if lora_id in _LOADED_ADAPTERS:
46
+ continue
47
+ repo = registry_by_id[lora_id]["hf_repo"]
48
+ pipe.load_lora_weights(repo, adapter_name=lora_id)
49
+ _LOADED_ADAPTERS.add(lora_id)
50
+
51
+
52
+ def compose(
53
+ lora_ids: list[str],
54
+ weights: list[float],
55
+ prompt: str,
56
+ registry_path: str = "registry/loras.json",
57
+ seed: int = 42,
58
+ output_dir: str = "outputs",
59
+ mode: str = "live",
60
+ width: int = 768,
61
+ height: int = 768,
62
+ num_inference_steps: int = 4,
63
+ guidance_scale: float = 1.0,
64
+ ) -> dict[str, Any]:
65
+ """Generate with FLUX.2 klein + LoRA adapters and return image/provenance.
66
+
67
+ `mode="live"` is the real path. `mode="mock"` was removed from the UI, but
68
+ kept as an explicit developer escape hatch only through NotImplementedError
69
+ so accidental demo mocks fail loudly.
70
+ """
71
+ if mode != "live":
72
+ raise NotImplementedError("Mock generation is disabled. Use mode='live'.")
73
+
74
+ import torch
75
+
76
+ if len(lora_ids) != len(weights):
77
+ raise ValueError("lora_ids and weights must be the same length")
78
+ if not prompt.strip():
79
+ raise ValueError("Prompt is required")
80
+
81
+ registry = load_registry(registry_path)
82
+ by_id = lora_map(registry)
83
+ missing = [lid for lid in lora_ids if lid not in by_id]
84
+ if missing:
85
+ raise KeyError(f"Unknown LoRA ids: {missing}")
86
+
87
+ weights = normalize_weights(weights)
88
+ pipe = _load_pipe()
89
+ _load_adapters(pipe, lora_ids, by_id)
90
+ pipe.set_adapters(lora_ids, adapter_weights=weights)
91
+
92
+ triggers = " ".join(by_id[lid]["trigger"] for lid in lora_ids)
93
+ full_prompt = f"{triggers}. {prompt.strip()}"
94
+
95
+ device, _ = _device()
96
+ generator = torch.Generator(device=device if device == "cuda" else "cpu").manual_seed(int(seed))
97
+ result = pipe(
98
+ prompt=full_prompt,
99
+ num_inference_steps=int(num_inference_steps),
100
+ guidance_scale=float(guidance_scale),
101
+ height=int(height),
102
+ width=int(width),
103
+ generator=generator,
104
+ )
105
+ image = result.images[0]
106
+
107
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
108
+ generation_id = f"{seed}_{'_'.join(lora_ids)}"
109
+ output_path = Path(output_dir) / f"{generation_id}.png"
110
+ image.save(output_path)
111
+
112
+ provenance = build_provenance(lora_ids, weights, registry, prompt, seed, str(output_path))
113
+ provenance["full_prompt"] = full_prompt
114
+ provenance["inference_model"] = BASE_INFERENCE_MODEL
115
+ provenance["mode"] = "live"
116
+ return {"image": image, "provenance": provenance, "output_path": str(output_path)}
compose/provenance.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ BASE_MODEL = "black-forest-labs/FLUX.2-klein-base-4B"
11
+
12
+
13
+ def load_registry(registry_path: str | Path = "registry/loras.json") -> dict[str, Any]:
14
+ with open(registry_path, "r", encoding="utf-8") as f:
15
+ return json.load(f)
16
+
17
+
18
+ def lora_map(registry: dict[str, Any]) -> dict[str, dict[str, Any]]:
19
+ return {entry["id"]: entry for entry in registry.get("loras", [])}
20
+
21
+
22
+ def expand_ancestry(
23
+ lora_id: str,
24
+ weight: float,
25
+ registry_by_id: dict[str, dict[str, Any]],
26
+ *,
27
+ max_depth: int = 8,
28
+ _depth: int = 0,
29
+ _seen: set[str] | None = None,
30
+ ) -> dict[str, Any]:
31
+ """Expand a LoRA node into a provenance tree.
32
+
33
+ Defaults to 8 levels: effectively complete for the hackathon, but bounded.
34
+ Circular references are marked instead of recursing forever.
35
+ """
36
+ seen = set() if _seen is None else set(_seen)
37
+ if lora_id not in registry_by_id:
38
+ raise KeyError(f"Unknown LoRA id: {lora_id}")
39
+
40
+ lora = registry_by_id[lora_id]
41
+ node = {
42
+ "lora_id": lora["id"],
43
+ "weight": round(float(weight), 4),
44
+ "weight_pct": round(float(weight) * 100, 2),
45
+ "creator": lora.get("creator"),
46
+ "cultural_source": lora.get("cultural_source"),
47
+ "hf_repo": lora.get("hf_repo"),
48
+ "checkpoint_step": lora.get("checkpoint_step"),
49
+ "type": lora.get("type"),
50
+ "parent_ids": lora.get("parent_ids", []),
51
+ "status": lora.get("status", "unknown"),
52
+ "children": [],
53
+ }
54
+
55
+ if lora_id in seen:
56
+ node["cycle_detected"] = True
57
+ return node
58
+ if _depth >= max_depth:
59
+ node["max_depth_reached"] = True
60
+ return node
61
+
62
+ seen.add(lora_id)
63
+ parent_ids = lora.get("parent_ids", [])
64
+ if parent_ids:
65
+ parent_weight = float(weight) / len(parent_ids)
66
+ node["children"] = [
67
+ expand_ancestry(
68
+ parent_id,
69
+ parent_weight,
70
+ registry_by_id,
71
+ max_depth=max_depth,
72
+ _depth=_depth + 1,
73
+ _seen=seen,
74
+ )
75
+ for parent_id in parent_ids
76
+ if parent_id in registry_by_id
77
+ ]
78
+ return node
79
+
80
+
81
+ def normalize_weights(weights: list[float]) -> list[float]:
82
+ total = sum(weights)
83
+ if total <= 0:
84
+ raise ValueError("Blend weights must sum to a positive number")
85
+ return [w / total for w in weights]
86
+
87
+
88
+ def file_sha256(path: str | Path | None) -> str | None:
89
+ if path is None:
90
+ return None
91
+ p = Path(path)
92
+ if not p.exists() or not p.is_file():
93
+ return None
94
+ h = hashlib.sha256()
95
+ with p.open("rb") as f:
96
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
97
+ h.update(chunk)
98
+ return "sha256:" + h.hexdigest()
99
+
100
+
101
+ def build_provenance(
102
+ lora_ids: list[str],
103
+ weights: list[float],
104
+ registry: dict[str, Any],
105
+ prompt: str,
106
+ seed: int,
107
+ output_path: str | None = None,
108
+ *,
109
+ max_depth: int = 8,
110
+ ) -> dict[str, Any]:
111
+ if len(lora_ids) != len(weights):
112
+ raise ValueError("lora_ids and weights must be the same length")
113
+
114
+ weights = normalize_weights(weights)
115
+ by_id = lora_map(registry)
116
+ ancestry = [
117
+ expand_ancestry(lora_id, weight, by_id, max_depth=max_depth)
118
+ for lora_id, weight in zip(lora_ids, weights)
119
+ ]
120
+
121
+ return {
122
+ "generation_id": str(uuid.uuid4()),
123
+ "timestamp": datetime.now(timezone.utc).isoformat(),
124
+ "prompt": prompt,
125
+ "seed": seed,
126
+ "base_model": BASE_MODEL,
127
+ "ancestry": ancestry,
128
+ "image_hash": file_sha256(output_path),
129
+ "output_path": output_path,
130
+ }
131
+
132
+
133
+ def provenance_sentence(provenance: dict[str, Any]) -> str:
134
+ parts = [
135
+ f"{node['weight_pct']:g}% {node['cultural_source']}"
136
+ for node in provenance.get("ancestry", [])
137
+ ]
138
+ if not parts:
139
+ return "No lineage data available yet."
140
+ return "This image blends " + " and ".join(parts) + "."
registry/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ def registry_path() -> Path:
9
+ return Path(__file__).with_name("loras.json")
10
+
11
+
12
+ def load_registry(path: str | Path | None = None) -> dict[str, Any]:
13
+ p = Path(path) if path else registry_path()
14
+ return json.loads(p.read_text(encoding="utf-8"))
15
+
16
+
17
+ def lora_choices(path: str | Path | None = None) -> list[tuple[str, str]]:
18
+ registry = load_registry(path)
19
+ choices = []
20
+ for lora in registry.get("loras", []):
21
+ label = f"{lora['cultural_source']} · {lora['id']} ({lora.get('status', 'unknown')})"
22
+ choices.append((label, lora["id"]))
23
+ return choices
registry/loras.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "loras": [
3
+ {
4
+ "id": "eritrean_krar_v1",
5
+ "trigger": "ERTRN_KRAR",
6
+ "hf_repo": "build-small-hackathon/visual-lineage-eritrean_krar_v1",
7
+ "checkpoint_step": 1500,
8
+ "type": "instrument",
9
+ "cultural_source": "Eritrean krar / Horn of Africa bowl lyre",
10
+ "creator": "projectmehari",
11
+ "parent_ids": [],
12
+ "status": "published",
13
+ "sensitivity": {
14
+ "requires_review": false
15
+ },
16
+ "local_artifact": "train/output/eritrean_krar_v1_fresh_20260614-111947/eritrean_krar_v1/eritrean_krar_v1.safetensors",
17
+ "sample_review": "Only valid generated samples downloaded for steps 1250/1500; final selected provisionally due deadline."
18
+ },
19
+ {
20
+ "id": "eritrean_heritage_v1",
21
+ "trigger": "ERTRN_HRTG",
22
+ "hf_repo": "projectmehari/visual-lineage-eritrean_heritage_v1",
23
+ "checkpoint_step": null,
24
+ "type": "heritage",
25
+ "cultural_source": "Eritrean visual heritage",
26
+ "creator": "projectmehari",
27
+ "parent_ids": [],
28
+ "status": "planned"
29
+ },
30
+ {
31
+ "id": "mtl_514_v1",
32
+ "trigger": "MTL_514",
33
+ "hf_repo": "projectmehari/visual-lineage-mtl_514_v1",
34
+ "checkpoint_step": null,
35
+ "type": "meme",
36
+ "cultural_source": "Montr\u00e9al 514 street culture",
37
+ "creator": "projectmehari",
38
+ "parent_ids": [],
39
+ "status": "planned"
40
+ },
41
+ {
42
+ "id": "knicks_in_4_v1",
43
+ "trigger": "KNKS_N4",
44
+ "hf_repo": "projectmehari/visual-lineage-knicks_in_4_v1",
45
+ "checkpoint_step": null,
46
+ "type": "meme",
47
+ "cultural_source": "Knicks playoff fan culture",
48
+ "creator": "projectmehari",
49
+ "parent_ids": [],
50
+ "status": "planned"
51
+ },
52
+ {
53
+ "id": "habesha_diaspora_v1",
54
+ "trigger": "HBSH_DSPR",
55
+ "hf_repo": "projectmehari/visual-lineage-habesha_diaspora_v1",
56
+ "checkpoint_step": null,
57
+ "type": "heritage",
58
+ "cultural_source": "Habesha diaspora visual culture",
59
+ "creator": "projectmehari",
60
+ "parent_ids": [],
61
+ "status": "planned",
62
+ "sensitivity": {
63
+ "requires_review": true
64
+ }
65
+ }
66
+ ]
67
+ }
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=5.0
2
+ pydantic>=2.0
3
+ PyYAML>=6.0
4
+ Pillow>=10.0
5
+ requests>=2.31
6
+ huggingface_hub>=0.26
7
+ peft>=0.14
8
+ accelerate>=1.0
9
+ torch
10
+ diffusers @ git+https://github.com/huggingface/diffusers.git
11
+ transformers
12
+ sentencepiece
13
+ protobuf
14
+ safetensors