Spaces:
Running
Running
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,
)
|