File size: 5,022 Bytes
a71fa9d
 
0e9675b
0d7b5a8
 
a71fa9d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e9675b
 
 
a71fa9d
 
 
 
 
 
 
0e9675b
 
a71fa9d
 
 
 
 
0e9675b
 
a71fa9d
 
 
0e9675b
 
 
a71fa9d
 
0e9675b
a71fa9d
0d7b5a8
a71fa9d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d7b5a8
a71fa9d
 
 
0d7b5a8
0e9675b
 
a71fa9d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import time, base64, io, os, requests
from typing import Optional
from PIL import Image
import gradio as gr

# =========================
# Stable Horde config
# =========================
HORDE_URL = "https://stablehorde.net/api/v2/generate/async"
HORDE_STATUS = "https://stablehorde.net/api/v2/generate/status/{id}"

# Optional: set an API key in your Space secrets for better priority:
# Settings → Repository secrets → add HORDE_API_KEY
HORDE_API_KEY = os.getenv("HORDE_API_KEY", "")

DEFAULT_STEPS = 24
DEFAULT_W = 768
DEFAULT_H = 768
POLL_INTERVAL = 2.5        # seconds between polls
POLL_TIMEOUT = 180         # max seconds to wait for a job
MODEL = None               # let Horde choose; or set e.g. "SDXL 1.0"

# =========================
# Horde client
# =========================
def horde_txt2img(prompt: str,
                  steps: int = DEFAULT_STEPS,
                  width: int = DEFAULT_W,
                  height: int = DEFAULT_H,
                  model: Optional[str] = MODEL) -> Image.Image:
    if not prompt or not prompt.strip():
        raise gr.Error("Please enter a prompt.")

    payload = {
        "prompt": prompt.strip(),
        "params": {
            "steps": int(steps),
            "width": int(width),
            "height": int(height),
            "n": 1
        },
        "nsfw": False,
        "censor_nsfw": True
    }
    if model:
        payload["models"] = [model]

    # Submit
    headers = {"apikey": HORDE_API_KEY} if HORDE_API_KEY else {}
    submit = requests.post(HORDE_URL, json=payload, headers=headers, timeout=30)
    submit.raise_for_status()
    job_id = submit.json()["id"]

    # Poll
    start = time.time()
    while True:
        status = requests.get(HORDE_STATUS.format(id=job_id), timeout=30).json()
        if status.get("faulted"):
            raise gr.Error(f"Horde job faulted: {status}")
        if status.get("done"):
            gens = status.get("generations") or []
            if not gens:
                raise gr.Error("Horde finished but returned no generations.")
            b64 = gens[0]["img"]
            img_bytes = base64.b64decode(b64)
            return Image.open(io.BytesIO(img_bytes)).convert("RGB")

        if time.time() - start > POLL_TIMEOUT:
            raise gr.Error("Timed out waiting for Horde. Try again or reduce steps/size.")
        time.sleep(POLL_INTERVAL)

# Wrapper for Gradio (adds optional negative prompt + long-shot toggle if you want later)
def generate_image(prompt, steps, size):
    # size like "768x768"
    try:
        w, h = [int(x.strip()) for x in size.lower().split("x")]
    except Exception:
        w, h = DEFAULT_W, DEFAULT_H
    img = horde_txt2img(prompt, steps=steps, width=w, height=h)
    return img

# =========================
# UI
# =========================
CUSTOM_CSS = """
.gradio-container { padding: 24px; }

/* Rounded prompt boxes */
.prompt-box textarea {
  border-radius: 18px !important;
  min-height: 90px;
  font-size: 16px;
  line-height: 1.4;
  padding: 14px 16px;
}

/* Pill buttons */
.pill button {
  border-radius: 999px !important;
  padding: 10px 18px;
  font-size: 15px;
}

/* Blue-ish image boxes feel (border radius) */
.image-out .wrap, .image-out .svelte-1ipelgc {
  border-radius: 22px !important;
}
"""

with gr.Blocks(css=CUSTOM_CSS, title="Stitch – Image Checkpoints (Stable Horde)") as demo:
    gr.Markdown("### Image Checkpoints (Stable Horde) — generate per-prompt frames")

    # Global controls (applies to all generators)
    with gr.Row():
        steps = gr.Slider(8, 50, value=DEFAULT_STEPS, step=1, label="Steps (quality/time)")
        size = gr.Dropdown(
            choices=["512x512", "768x768", "1024x576", "1024x768"],
            value=f"{DEFAULT_W}x{DEFAULT_H}",
            label="Resolution"
        )

    # 4 rows: [Prompt + Button]  |  [Image Output]
    prompt_boxes = []
    gen_buttons = []
    img_outputs = []

    for i in range(1, 5):
        with gr.Row():
            with gr.Column(scale=1, min_width=320):
                p = gr.Textbox(
                    placeholder=f"Prompt input (Image {i})",
                    lines=4,
                    label=None,
                    elem_classes=["prompt-box"]
                )
                b = gr.Button(f"Generate image {i}", elem_classes=["pill"])
            with gr.Column(scale=2, min_width=380):
                img = gr.Image(label=f"Image {i} output", type="pil", elem_classes=["image-out"])
        prompt_boxes.append(p)
        gen_buttons.append(b)
        img_outputs.append(img)

    # Wire callbacks
    for i in range(4):
        gen_buttons[i].click(
            fn=generate_image,
            inputs=[prompt_boxes[i], steps, size],
            outputs=[img_outputs[i]]
        )

    gr.Markdown(
        "> Tip: You can generate images independently per row. Steps ↑ = higher quality but slower. "
        "Stable Horde is free (crowd GPUs), so expect occasional queue time."
    )

if __name__ == "__main__":
    demo.queue().launch()