File size: 8,640 Bytes
b3a50e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59a45a0
b3a50e3
59a45a0
b3a50e3
 
59a45a0
 
 
 
 
 
b3a50e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59a45a0
 
 
 
 
 
 
 
b3a50e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59a45a0
 
 
 
 
 
 
 
 
b3a50e3
 
 
 
 
 
 
 
 
 
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
161
162
163
164
165
166
167
168
169
170
171
172
"""Personalized text-to-image website. Requires Sumith's hosted model weights."""
import os
import secrets
import threading
from pathlib import Path

import spaces
import gradio as gr

from prompts import make_prompt, STYLES

_pipeline = None
_lock = threading.Lock()


def load_pipeline():
    global _pipeline
    if _pipeline is not None:
        return _pipeline
    import torch
    from diffusers import StableDiffusionPipeline, DDIMScheduler
    from huggingface_hub import hf_hub_download

    local_path = os.getenv("MODEL_PATH", "")
    repo = os.getenv("MODEL_REPO_ID", "Sumith0102/PersonalisedAiAvatarGenerator")
    if local_path:
        path = Path(local_path)
    elif repo:
        path = Path(hf_hub_download(
            repo_id=repo,
            filename=os.getenv("MODEL_FILENAME", "model.ckpt"),
            token=os.getenv("HF_TOKEN") or None,
        ))
    else:
        raise gr.Error("The owner is connecting the personalized model. Please try again later.")
    if path.suffix == ".ckpt":
        from convert_checkpoint import convert
        import hashlib
        import tempfile
        cache_key = hashlib.sha256(str(path.resolve()).encode()).hexdigest()[:20]
        safe_path = Path(tempfile.gettempdir()) / f"sumith-{cache_key}.safetensors"
        if not safe_path.exists():
            convert(path, safe_path)
        path = safe_path
    elif path.suffix != ".safetensors":
        raise gr.Error("The configured model format is not supported.")
    pipeline = StableDiffusionPipeline.from_single_file(
        str(path),
        config=os.getenv("BASE_MODEL_ID", "stable-diffusion-v1-5/stable-diffusion-v1-5"),
        torch_dtype=torch.float16,
        token=os.getenv("HF_TOKEN") or None,
        load_safety_checker=True,
    )
    pipeline.scheduler = DDIMScheduler.from_config(pipeline.scheduler.config)
    pipeline.enable_attention_slicing()
    pipeline.enable_vae_slicing()
    pipeline.to("cuda")
    _pipeline = pipeline
    return pipeline


def prepare_prompt(scene, style, enhance=True):
    try:
        return make_prompt(scene, style, load_pipeline().tokenizer, enhance)
    except ValueError as exc:
        raise gr.Error(str(exc)) from None


@spaces.GPU(duration=60)
def generate(scene, style, seed, steps, enhance=True, progress=gr.Progress()):
    import torch
    prompt = prepare_prompt(scene, style, enhance)
    if seed is None:
        raise gr.Error("Enter a seed, or use -1 for a random image.")
    if isinstance(seed, bool) or int(seed) != seed or not -1 <= seed <= 2147483647:
        raise gr.Error("Choose a whole-number seed from 0 to 2147483647, or -1 for random.")
    seed = secrets.randbelow(2147483648) if seed == -1 else int(seed)
    steps = int(steps)
    if not 15 <= steps <= 35:
        raise gr.Error("Use between 15 and 35 generation steps.")
    with _lock:
        try:
            progress(0.05, desc="Loading the personalized model")
            pipe = load_pipeline()
            device = "cuda"
            rng = torch.Generator(device=device).manual_seed(seed)
            def step_callback(pipeline, index, timestep, kwargs):
                progress(0.1 + 0.85 * (index + 1) / steps, desc="Creating your portrait")
                return kwargs
            with torch.inference_mode():
                result = pipe(
                    prompt=prompt,
                    negative_prompt="blurry, low resolution, distorted face, duplicate person, watermark, text",
                    height=512, width=512, guidance_scale=7.5,
                    num_inference_steps=steps, generator=rng,
                    callback_on_step_end=step_callback,
                )
            if result.nsfw_content_detected and any(result.nsfw_content_detected):
                raise gr.Error("This image could not be displayed. Please try another scene.")
            return result.images[0], f"Seed: {seed} 路 512 脳 512 路 AI-generated image"
        except gr.Error:
            raise
        except Exception as exc:
            # Do not return upstream paths, tokens, prompts, or credentials to visitors.
            print("Generation failed:", type(exc).__name__, flush=True)
            raise gr.Error("Generation is unavailable right now. Please try again later.") from None


CSS = """
.gradio-container {max-width: 1240px !important; margin: auto; background: #f5f7fc;}
#studio-header {padding: 28px 0 14px; border-bottom: 1px solid #cdd5e6; margin-bottom: 22px;}
#studio-header h1 {font-size: clamp(30px, 4vw, 48px); letter-spacing: -1.5px; color: #182448;}
#studio-header p {font-size: 16px; color: #475777;}
#generate {background: #304ffe; color: white; border: 0; min-height: 52px; font-size: 17px;}
#portrait {min-height: 460px; background: #e9eef9; border-radius: 18px;}
#footer {font-size: 14px; color: #566581; margin-top: 20px;}
"""

with gr.Blocks(title="Sumith Portrait Studio",
               analytics_enabled=False, delete_cache=(3600, 3600)) as demo:
    gr.Markdown("# Sumith Portrait Studio\nDescribe a scene. Create an AI portrait of Sumith.", elem_id="studio-header")
    with gr.Row():
        with gr.Column(scale=5):
            scene = gr.Textbox(label="Your scene", lines=5, max_lines=8,
                placeholder="Standing beside a lake in the Canadian mountains at sunrise")
            style = gr.Radio(list(STYLES), value="Natural", label="Look")
            enhance = gr.Checkbox(value=True, label="Add visual detail to my prompt")
            gr.Markdown("A lightweight prompt assistant adds lighting and composition cues where there is room. "
                        "Your scene stays intact. No extra AI service is used.")
            preview = gr.Button("Preview generation prompt", variant="secondary")
            final_prompt = gr.Textbox(label="Exact generation prompt", lines=4, interactive=False,
                                      placeholder="Preview your prompt, or create a portrait to see it here.")
            gr.Markdown("The model has a short text limit. Optional details only fill the remaining space; "
                        "overlong scenes are rejected, never silently cut off. Turn enhancement off for just your scene and the identity cue.")
            with gr.Accordion("Fine-tune the result", open=False):
                seed = gr.Number(value=-1, precision=0, label="Seed 路 -1 for a new variation")
                steps = gr.Slider(15, 35, value=24, step=1, label="Generation steps")
            submit = gr.Button("Create portrait", variant="primary", elem_id="generate")
            gr.Examples([
                ["Walking through Old Montr茅al on a sunny afternoon", "Natural"],
                ["In a tailored navy suit, soft studio lighting, professional headshot", "Studio"],
                ["An explorer beside a mountain lake at sunrise", "Cinematic"],
            ], inputs=[scene, style], label="Try a scene", cache_examples=False)
        with gr.Column(scale=6):
            portrait = gr.Image(label="Your portrait", type="pil", interactive=False,
                                buttons=["download"], elem_id="portrait")
            details = gr.Markdown("Your generated portrait will appear here.")
    gr.Markdown(
        "Built by Sumith Reddy 路 DreamBooth / Stable Diffusion 1.5  \n"
        "Images are AI-generated and may vary in likeness. Prompts and results are processed by the hosting service. "
        "The app keeps temporary output files for up to about two hours.  \n"
        "[Back to portfolio](https://thanu-sumith.github.io/sumith-portfolio/)", elem_id="footer")
    preview.click(prepare_prompt, inputs=[scene, style, enhance], outputs=final_prompt,
                  api_name="preview_prompt")
    # Clear stale previews when the request changes.
    for control in [scene, style, enhance]:
        control.change(lambda: "", outputs=final_prompt, queue=False, api_name=False)
    submit.click(prepare_prompt, inputs=[scene, style, enhance], outputs=final_prompt,
                 api_name=False).success(
        generate, inputs=[scene, style, seed, steps, enhance], outputs=[portrait, details],
        concurrency_limit=1, api_name="generate_portrait")

# Prepare the model before ZeroGPU forks a worker. Its CUDA emulation supports
# model placement at startup; real GPU computation happens only in generate().
if __name__ == "__main__":
    load_pipeline()
    demo.queue(max_size=4, default_concurrency_limit=1).launch(
        css=CSS, theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
        server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")),
        show_error=False, max_file_size="1mb",
    )