from __future__ import annotations
import html
import json
import sys
import traceback
from pathlib import Path
import gradio as gr
try:
import spaces
except Exception: # local dev outside HF Spaces
class _SpacesShim:
@staticmethod
def GPU(*args, **kwargs):
def deco(fn):
return fn
return deco
spaces = _SpacesShim()
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from compose.merge import compose
from compose.provenance import provenance_sentence
from registry import lora_choices
SAGE = "#5f6f52"
CREAM = "#f6f1e8"
TERRACOTTA = "#b7653c"
def render_node(node: dict, depth: int = 0) -> str:
margin = depth * 22
children = "".join(render_node(child, depth + 1) for child in node.get("children", []))
status = node.get("status", "unknown")
checkpoint = node.get("checkpoint_step") or "not selected yet"
cycle = "
Cycle detected — recursion stopped.
" if node.get("cycle_detected") else ""
return f"""
{html.escape(node.get('cultural_source') or node['lora_id'])}
{node['weight_pct']:g}%
{html.escape(node['lora_id'])} · {html.escape(str(node.get('type')))} · {html.escape(str(status))}
checkpoint: {html.escape(str(checkpoint))}
{html.escape(node.get('hf_repo') or '')}
{cycle}
{children}
"""
def render_lineage(provenance: dict) -> str:
ancestry = provenance.get("ancestry", [])
bars = []
colors = [SAGE, TERRACOTTA, "#3f5f8f", "#9a7b33", "#72517e"]
for i, node in enumerate(ancestry):
bars.append(
f"{node['weight_pct']:g}%
"
)
cards = "".join(render_node(node) for node in ancestry)
raw = html.escape(json.dumps(provenance, indent=2))
sentence = html.escape(provenance_sentence(provenance))
full_prompt = html.escape(provenance.get("full_prompt", ""))
model = html.escape(provenance.get("inference_model", provenance.get("base_model", "")))
return f"""
Visual lineage
{sentence}
Model: {model}
Full prompt: {full_prompt}
{''.join(bars)}
{cards}
Raw provenance JSON
{raw}
"""
@spaces.GPU(duration=180)
def generate(lora_a: str, lora_b: str, blend_a: int, prompt: str, seed: int, size: int):
if not prompt.strip():
raise gr.Error("Give the image a prompt first.")
weight_a = blend_a / 100
weight_b = 1 - weight_a
try:
result = compose(
lora_ids=[lora_a, lora_b],
weights=[weight_a, weight_b],
prompt=prompt,
seed=int(seed),
registry_path=str(ROOT / "registry/loras.json"),
output_dir=str(ROOT / "outputs"),
width=int(size),
height=int(size),
)
return result["image"], render_lineage(result["provenance"])
except Exception as e:
traceback.print_exc()
raise gr.Error(f"Generation failed: {type(e).__name__}: {e}")
def build_demo():
choices = lora_choices(ROOT / "registry/loras.json")
default_a = "eritrean_krar_v1" if any(v == "eritrean_krar_v1" for _, v in choices) else choices[0][1]
default_b = default_a
with gr.Blocks(title="Visual Lineage", theme=gr.themes.Soft(primary_hue="green")) as demo:
gr.Markdown(
"# Visual Lineage\n"
"*A 23andMe for imagined instruments.*\n\n"
"Live FLUX.2 [klein] inference with published instrument LoRAs. "
"Select any two from Eritrean krar, Korean gayageum, or Brazilian berimbau "
"and trace the visual lineage of every generation."
)
with gr.Row():
with gr.Column(scale=1):
lora_a = gr.Dropdown(label="Instrument/source A", choices=choices, value=default_a)
lora_b = gr.Dropdown(label="Instrument/source B", choices=choices, value=default_b)
blend = gr.Slider(0, 100, value=100, step=1, label="Blend: A % / B %")
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)
seed = gr.Number(label="Seed", value=42, precision=0)
size = gr.Slider(512, 1024, value=768, step=256, label="Image size")
btn = gr.Button("Generate with live LoRA", variant="primary")
with gr.Column(scale=2):
output_image = gr.Image(label="Generated image", type="pil")
lineage_panel = gr.HTML(label="Visual lineage")
btn.click(generate, [lora_a, lora_b, blend, prompt, seed, size], [output_image, lineage_panel], show_progress="full")
return demo
if __name__ == "__main__":
build_demo().launch()