Spaces:
Runtime error
Runtime error
File size: 7,853 Bytes
e4153e0 d14c303 e4153e0 d14c303 e4153e0 4cb455d e4153e0 d14c303 e4153e0 d14c303 e4153e0 d14c303 e4153e0 d14c303 e4153e0 d14c303 e4153e0 d14c303 e4153e0 d14c303 e4153e0 d14c303 e4153e0 d14c303 e4153e0 d14c303 e4153e0 d14c303 e4153e0 d14c303 | 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 173 174 175 176 177 178 | """
Point d'entree du microservice.
Architecture de reference (dev, GPU dedie - pod) : une app FastAPI unique qui
1. expose un endpoint WebSocket /ws/edit (protocole reutilisable par
n'importe quel client - pas seulement l'UI de demo),
2. monte l'UI Gradio sur "/" dans le MEME process Python (`gradio.mount_gradio_app`),
pour que @spaces.GPU (Hugging Face Spaces + ZeroGPU) reste valide - le
decorateur attend d'etre appele depuis le process reconnu comme le Space.
Repli sous ZeroGPU reel (voir projet_2_avancement.md, Etape 6) : ce montage
FastAPI+uvicorn.run() custom demarre correctement (modeles charges, port
lie) puis le process est arrete silencieusement quelques centaines de ms
apres, sans traceback - tous les exemples ZeroGPU officiels utilisent
`demo.launch()` directement, jamais un uvicorn.run() manuel, et le
superviseur ZeroGPU semble s'attendre a ce cycle de vie precis. Sous
ZeroGPU (detecte via pipeline.IS_ZERO_GPU), on bascule donc sur un
`demo.launch()` Gradio natif, SANS le endpoint WebSocket - le protocole
FastAPI+WebSocket reste demontre et valide en dev, mais n'est pas ce qui
tourne sur le Space public.
"""
import os
# DOIT s'executer avant TOUT import touchant huggingface_hub (gradio inclus -
# gradio importe huggingface_hub en interne). huggingface_hub fige HF_HOME
# comme constante de module des son propre import ; le corriger plus tard
# (ex. dans pipeline.py, importe apres gradio) ne change plus rien, la
# constante est deja calculee avec l'ancienne valeur. Cf. PermissionError
# rencontree au premier deploiement Spaces : /home/user/.cache/huggingface
# est ecrit pendant le BUILD (preload_from_hub) mais pas inscriptible par
# l'utilisateur runtime - Florence-2 (trust_remote_code) a besoin d'ecrire
# dans <HF_HOME>/modules/.
_hf_home = os.environ.get("HF_HOME", os.path.expanduser("~/.cache/huggingface"))
try:
os.makedirs(os.path.join(_hf_home, "modules"), exist_ok=True)
except PermissionError:
os.environ["HF_HOME"] = "/tmp/hf_home"
print(f"[app] HF_HOME ({_hf_home}) non inscriptible au runtime -> repli sur /tmp/hf_home")
import base64
import io
import json
import gradio as gr
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from PIL import Image
from pydantic import ValidationError
import pipeline
import runner
from gradio_ui import build_ui
from ws_protocol import ConfirmRequest, LocateRequest
def _b64_to_image(b64: str) -> Image.Image:
return Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")
def _image_to_b64(img: Image.Image) -> str:
buf = io.BytesIO()
img.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode()
def _build_websocket_app() -> FastAPI:
"""Architecture de reference : FastAPI + endpoint WebSocket /ws/edit +
UI Gradio montee dans le meme process (cf. docstring en tete de fichier).
Utilisee en dev (GPU dedie) ; PAS utilisee sous ZeroGPU (voir _main)."""
app = FastAPI(title="genai-image-editor")
@app.websocket("/ws/edit")
async def ws_edit(websocket: WebSocket):
await websocket.accept()
# Etat de session : le contexte (image + masque valide) vit le temps
# de cette connexion, entre la phase "locate" et la phase "confirm".
session = {"image": None, "dilated_mask": None}
try:
while True:
raw = await websocket.receive_text()
try:
payload = json.loads(raw)
except json.JSONDecodeError:
await websocket.send_json({"type": "error", "message": "JSON invalide."})
continue
msg_type = payload.get("type")
if msg_type == "locate":
try:
req = LocateRequest.model_validate(payload)
except ValidationError as e:
await websocket.send_json({"type": "error", "message": str(e)})
continue
image = _b64_to_image(req.image_b64)
session["image"] = image
try:
result = await runner.run_locate(image, req.text_query)
except Exception as e:
await websocket.send_json({"type": "error", "message": str(e)})
continue
session["dilated_mask"] = Image.open(io.BytesIO(result.dilated_mask_png)).convert("L")
await websocket.send_json({
"type": "mask_ready",
"mask_b64": base64.b64encode(result.dilated_mask_png).decode(),
"box": result.box,
"clip_confidence": result.clip_confidence,
})
elif msg_type == "confirm":
try:
req = ConfirmRequest.model_validate(payload)
except ValidationError as e:
await websocket.send_json({"type": "error", "message": str(e)})
continue
if session["image"] is None or session["dilated_mask"] is None:
await websocket.send_json({
"type": "error",
"message": "Aucun masque en attente - envoyer 'locate' d'abord.",
})
continue
try:
async for kind, *rest in runner.run_inpaint_stream(
session["image"], session["dilated_mask"],
req.prompt, req.negative_prompt,
req.steps, req.guidance_scale, req.seed,
):
if kind == "progress":
step, total = rest
await websocket.send_json({
"type": "progress", "stage": "diffusion", "step": step, "total": total,
})
elif kind == "result":
(image_result,) = rest
await websocket.send_json({
"type": "result", "image_b64": _image_to_b64(image_result),
})
except Exception as e:
await websocket.send_json({"type": "error", "message": str(e)})
else:
await websocket.send_json({"type": "error", "message": f"Type de message inconnu : {msg_type}"})
except WebSocketDisconnect:
pass
# ssr_mode=False : sans ca, Gradio 5.x demarre un serveur Node.js
# compagnon (rendu cote serveur du frontend) qui tente de se binder sur
# un port separe (7861, avec repli 7862...) - source d'un "address
# already in use" observe lors du premier essai de deploiement Spaces.
gr.mount_gradio_app(app, build_ui(), path="/", ssr_mode=False)
return app
if pipeline.IS_ZERO_GPU:
# Repli ZeroGPU (cf. docstring) : demo.launch() natif, pattern garanti
# compatible par tous les exemples officiels, sans le endpoint WebSocket.
if __name__ == "__main__":
demo = build_ui()
demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))
else:
# Architecture de reference (dev). `app` doit exister au niveau module
# pour `uvicorn app:app --port 8000` (voir README pod).
app = _build_websocket_app()
if __name__ == "__main__":
# Point d'entree si jamais execute directement (python app.py) hors
# ZeroGPU - en dev on utilise plutot `uvicorn app:app --port 8000`.
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
|