File size: 5,466 Bytes
648b1d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aa53aba
 
 
 
0132c34
aa53aba
 
 
 
 
 
0132c34
648b1d6
 
 
aa53aba
648b1d6
 
 
 
 
 
0132c34
 
 
648b1d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0132c34
648b1d6
0132c34
648b1d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import io
import tempfile

import gradio as gr
import requests
from PIL import Image

import fal_client

APP_NAME = os.getenv("APP_NAME", "Scenora")
MODEL_ID = os.getenv("MODEL_ID", "fal-ai/flux-pro/kontext")
APP_PASSWORD = os.getenv("APP_PASSWORD")

PROMPT_SUFFIX = (
    "commercial product photography, natural light, sharp focus on the product, "
    "realistic shadows and reflections, 24mm lens"
)

PRESETS = {
    "Studio neutre": "on a seamless light grey studio backdrop, soft diffused lighting",
    "Table en bois": "placed on a warm oak table, blurred kitchen in the background, morning light",
    "Beton mineral": "on a raw concrete surface, minimalist industrial setting, side light",
    "Exterieur nature": "on a mossy stone outdoors, soft green foliage behind, golden hour",
    "Marbre luxe": "on a white marble slab, elegant premium setting, soft reflections",
}

NEGATIVE_DEFAULT = "text, watermark, logo, people, clutter, distorted proportions"


def _image_to_url(image: Image.Image) -> str:
    """Envoie l'image sur le stockage temporaire de fal et renvoie son URL."""
    with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
        path = tmp.name
    image.convert("RGB").save(path, format="PNG")
    try:
        return fal_client.upload_file(path)
    finally:
        try:
            os.unlink(path)
        except OSError:
            pass


def _download(url: str) -> Image.Image:
    resp = requests.get(url, timeout=120)
    resp.raise_for_status()
    return Image.open(io.BytesIO(resp.content)).convert("RGB")


def generate(image, preset, prompt, negative, variations, progress=gr.Progress()):
    if not os.getenv("FAL_KEY"):
        raise gr.Error(
            "Cle API absente. Ajoutez FAL_KEY dans les Secrets du Space, "
            "puis redemarrez-le."
        )
    if image is None:
        raise gr.Error("Chargez d'abord une photo produit.")

    scene = prompt.strip() or PRESETS.get(preset, "")
    if not scene:
        raise gr.Error("Decrivez la scene souhaitee ou choisissez un preset.")

    instruction = (
        f"Replace the background only. Keep the product exactly as it is: "
        f"same shape, same colors, same proportions, same details. "
        f"New background: {scene}. Style: {PROMPT_SUFFIX}."
    )
    if negative.strip():
        instruction += f" Avoid: {negative.strip()}."

    progress(0, desc="Envoi de l'image...")
    image_url = _image_to_url(image)

    results = []
    n = int(variations)
    for i in range(n):
        progress((i + 0.2) / n, desc=f"Generation {i + 1}/{n}...")
        try:
            out = fal_client.subscribe(
                MODEL_ID,
                arguments={"prompt": instruction, "image_url": image_url},
            )
        except Exception as exc:
            raise gr.Error(f"Erreur API ({MODEL_ID}) : {exc}")

        images = out.get("images") or []
        if not images:
            raise gr.Error(f"Reponse vide du modele : {out}")

        progress((i + 0.8) / n, desc=f"Recuperation {i + 1}/{n}...")
        results.append(_download(images[0]["url"]))

    return results, instruction


CSS = """
html, body, .gradio-container {
    width: 100% !important;
    max-width: 100% !important;
}
.gradio-container {
    margin: 0 !important;
    padding: 1rem 1.5rem 2rem !important;
}
.main, .wrap, .contain, .app {
    max-width: 100% !important;
    width: 100% !important;
}
footer {display: none !important;}
"""

with gr.Blocks(css=CSS, title=APP_NAME, fill_width=True, fill_height=True) as demo:
    gr.Markdown(f"# {APP_NAME}")
    gr.Markdown(
        "Chargez une photo produit, choisissez ou decrivez le decor, generez. "
        "Le produit est preserve, seul l'arriere-plan change."
    )

    with gr.Row(equal_height=True):
        with gr.Column(scale=1, min_width=320):
            image_in = gr.Image(label="Photo produit", type="pil", height=420)
            preset = gr.Radio(
                label="Preset de decor",
                choices=list(PRESETS.keys()),
                value="Studio neutre",
            )
            prompt = gr.Textbox(
                label="Decor sur mesure (prioritaire sur le preset)",
                placeholder="sur un plan de travail en marbre, cuisine lumineuse en arriere-plan",
                lines=2,
            )
            negative = gr.Textbox(
                label="A eviter",
                value=NEGATIVE_DEFAULT,
                lines=2,
            )
            variations = gr.Slider(
                label="Nombre de variantes",
                minimum=1, maximum=4, step=1, value=2,
            )
            run = gr.Button("Generer", variant="primary")

        with gr.Column(scale=1, min_width=320):
            gallery = gr.Gallery(
                label="Resultats", columns=2, height=520,
                object_fit="contain", show_download_button=True,
            )
            debug = gr.Textbox(label="Instruction envoyee au modele", lines=4)

    run.click(
        fn=generate,
        inputs=[image_in, preset, prompt, negative, variations],
        outputs=[gallery, debug],
    )

    gr.Markdown(f"Modele : `{MODEL_ID}` — facture a l'image par le fournisseur.")

if __name__ == "__main__":
    auth = ("admin", APP_PASSWORD) if APP_PASSWORD else None
    demo.queue(max_size=8).launch(
        server_name="0.0.0.0",
        server_port=int(os.getenv("PORT", 7860)),
        auth=auth,
        show_api=False,
    )