File size: 6,188 Bytes
4ff39a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e6f305
 
 
 
 
 
80cb2a6
 
4ff39a4
 
 
 
 
 
 
 
 
 
 
 
 
2d71939
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e6f305
 
4ff39a4
 
 
 
0e6f305
 
 
 
4ff39a4
 
0e6f305
 
 
 
 
 
 
 
 
 
 
 
 
4ff39a4
 
 
80cb2a6
 
 
4ff39a4
 
 
 
 
 
 
 
80cb2a6
 
4ff39a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2d71939
 
 
 
 
4ff39a4
 
 
2d71939
 
 
 
 
 
 
 
 
4ff39a4
 
 
 
 
2d71939
 
 
 
 
4ff39a4
 
 
 
 
 
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
159
160
"""Inference backend for the DiffusionDB SD 1.5 LoRA.

A Gradio app whose only job is to expose generation as an API. The UI Space
(spaces/whosouravsharma/diffusiondb-sd15-lora) calls it with gradio_client;
the minimal interface here is also usable directly for debugging.

This is the Space that needs the GPU. Weights are pulled from the model repo
at first request, not baked into the image.
"""

import os

import gradio as gr

from src.pipeline import LORA_CHECKPOINT, LORA_REPO, ModelRunner, pick_device
from src.tracing import (
    TRACING_ACTIVE,
    configure_logging,
    continue_trace,
    logger,
)

configure_logging()

# Loaded on first request rather than at import, so the Space becomes
# reachable immediately and a slow weight download cannot kill startup.
_runner = None


def get_runner() -> ModelRunner:
    global _runner
    if _runner is None:
        _runner = ModelRunner()
    return _runner


def model_is_warm() -> bool:
    return _runner is not None


# Status strings for the visible UI. The cold path is genuinely slow -- the
# container has to wake, then torch/diffusers import, then ~9 s of weight
# loading before a single step runs -- so say so rather than showing a
# spinner that looks indistinguishable from a hang.
STATUS_IDLE = "Ready."
STATUS_COLD = (
    "⏳ **Starting server…** waking the GPU and loading the model. "
    "The first image takes around 40 seconds; later ones take a few."
)
STATUS_WARM = "⏳ **Generating…**"


def status_on_click() -> str:
    """Fires the instant the button is pressed, before the GPU job queues."""
    return STATUS_WARM if model_is_warm() else STATUS_COLD


def status_on_load() -> str:
    """Fires when someone opens the page."""
    if model_is_warm():
        return STATUS_IDLE
    return (
        "Model not loaded yet — the first generation also starts the server "
        "and takes around 40 seconds."
    )


def generate(prompt, negative_prompt, steps, guidance, lora_scale, seed,
             request: gr.Request = None):
    """One image per call. Returns (image, seed_used).

    Deliberately single-image: gradio_client handles a plain Image return
    far more predictably than a Gallery, and the UI can call again for more.

    `request` is supplied by Gradio from the type annotation and is not a UI
    input, so the published /generate signature stays at six arguments. It
    exists only to pick up the caller's trace headers.
    """
    try:
        # When the UI Space sent trace headers this run becomes a child of
        # its run, so one LangSmith trace covers UI and backend -- and the
        # visitor's location, carried in baggage, lands on this run too.
        with continue_trace(request):
            result = get_runner().generate(
                prompt=prompt,
                negative_prompt=negative_prompt,
                steps=int(steps),
                guidance=float(guidance),
                lora_scale=float(lora_scale),
                seed=None if int(seed) < 0 else int(seed),
                count=1,
            )
    except ValueError as error:
        raise gr.Error(str(error))
    except Exception as error:
        # The gr.Error string is all the caller sees; without this the
        # traceback is lost and the Space logs show nothing useful.
        logger.exception("request failed")
        raise gr.Error(f"Generation failed: {error}")
    return result.images[0], result.seed

with gr.Blocks(title="DiffusionDB SD 1.5 LoRA — inference API") as demo:
    gr.Markdown(
        f"## Inference API\n"
        f"Backend for [`diffusiondb-sd15-lora`]"
        f"(https://huggingface.co/spaces/whosouravsharma/diffusiondb-sd15-lora).\n\n"
        f"Model `{LORA_REPO}` @ `{LORA_CHECKPOINT}` · device `{pick_device()}` · "
        f"tracing `{'on' if TRACING_ACTIVE else 'off'}`\n\n"
        f"Call it with `gradio_client`:\n"
        f"```python\n"
        f"from gradio_client import Client\n"
        f"c = Client('whosouravsharma/diffusiondb-sd15-lora-inference')\n"
        f"image, seed = c.predict('a steampunk owl', '', 25, 7.5, 1.0, 42,\n"
        f"                        api_name='/generate')\n"
        f"```"
    )
    with gr.Row():
        with gr.Column():
            prompt = gr.Textbox(label="prompt", lines=2)
            negative = gr.Textbox(label="negative_prompt", lines=1)
            steps = gr.Number(value=25, label="steps", precision=0)
            guidance = gr.Number(value=7.5, label="guidance")
            lora_scale = gr.Number(value=1.0, label="lora_scale")
            seed = gr.Number(value=-1, label="seed", precision=0)
            run = gr.Button("Generate", variant="primary")
        with gr.Column():
            # Initial value assumes cold, because at import time it always
            # is; demo.load corrects it to "Ready." on a warm server. The
            # reverse default would flash a false "Ready." at every cold
            # visitor before the load event lands.
            status = gr.Markdown(status_on_load())
            image = gr.Image(label="image", type="pil")
            used_seed = gr.Number(label="seed_used", interactive=False)

    # Two independent listeners on the same button. The cosmetic one uses
    # queue=False so it paints immediately instead of waiting behind the GPU
    # job it is announcing. The API contract of /generate is untouched: same
    # six inputs, same two outputs, so gradio_client callers are unaffected.
    # api_name=False keeps these cosmetic events out of the published API,
    # so gradio_client sees /generate and nothing else.
    run.click(status_on_click, outputs=status, queue=False, api_name=False)

    generate_event = run.click(
        generate,
        inputs=[prompt, negative, steps, guidance, lora_scale, seed],
        outputs=[image, used_seed],
        api_name="generate",
    )
    generate_event.then(
        lambda: STATUS_IDLE, outputs=status, queue=False, api_name=False
    )

    demo.load(status_on_load, outputs=status, api_name=False)

if __name__ == "__main__":
    demo.queue(max_size=12).launch(
        server_name="0.0.0.0",
        server_port=int(os.environ.get("PORT", 7860)),
    )