sync: HF InferenceClient image generation fix

#51
by Baida07 - opened
Files changed (2) hide show
  1. api/vision.py +32 -52
  2. tests/test_vision_hf_only.py +10 -5
api/vision.py CHANGED
@@ -18,7 +18,7 @@ Fallback chain analyze_image:
18
  3. HF BLIP VQA + captioning (richiede solo HF_TOKEN)
19
 
20
  Image generation and editing use exclusively Hugging Face Inference API:
21
- - FLUX.1-schnell for text-to-image generation
22
  - FLUX.1-Kontext-dev for prompt-guided image editing
23
  """
24
  import asyncio, base64, io, os, httpx, logging
@@ -35,11 +35,11 @@ _HF_API = "https://router.huggingface.co/hf-inference"
35
  _USER_AGENT = "Mozilla/5.0 (compatible; AgenteAI/3.0)"
36
 
37
  _MODEL_MAP: dict[str, str] = {
38
- "FLUX.1-schnell": "black-forest-labs/FLUX.1-schnell",
39
- "FLUX.1-dev": "black-forest-labs/FLUX.1-dev",
40
- "sdxl": "stabilityai/stable-diffusion-xl-base-1.0",
41
- "flux": "black-forest-labs/FLUX.1-schnell",
42
- "flux-schnell": "black-forest-labs/FLUX.1-schnell",
43
  }
44
 
45
  _EDIT_MODEL = "timbrooks/instruct-pix2pix"
@@ -93,53 +93,33 @@ async def generate_image(req: GenerateImageRequest):
93
  - HF restituisce raw bytes PNG β€” non JSON.
94
  - steps ottimali FLUX.1-schnell: 4 (veloce) – 8 (qualitΓ ).
95
  """
96
- model_id = _MODEL_MAP.get(req.model, "black-forest-labs/FLUX.1-schnell")
97
- url = f"{_HF_API}/models/{model_id}"
98
-
99
- payload: dict = {"inputs": req.prompt.strip()[:400]}
100
- params: dict = {"num_inference_steps": min(max(req.steps, 1), 8)}
101
- if req.width != 512: params["width"] = min(max(req.width, 256), 1024)
102
- if req.height != 512: params["height"] = min(max(req.height, 256), 1024)
103
- if req.negative_prompt:
104
- params["negative_prompt"] = req.negative_prompt[:200]
105
- payload["parameters"] = params
106
-
107
- async with httpx.AsyncClient(timeout=90) as client:
108
- for attempt in range(2):
109
- try:
110
- r = await client.post(url, headers=_hf_headers(), json=payload)
 
111
 
112
- if r.status_code == 200:
113
- b64 = base64.b64encode(r.content).decode()
114
- return {
115
- "ok": True, "image_b64": b64, "mime": "image/png",
116
- "model": req.model, "prompt": req.prompt[:100],
117
- }
118
-
119
- if r.status_code == 503 and attempt == 0:
120
- try:
121
- wait = min(float(r.json().get("estimated_time", 20)), 45)
122
- except Exception:
123
- wait = 20
124
- _logger.info("HF model loading, waiting %.0fs…", wait)
125
- await asyncio.sleep(wait)
126
- continue
127
-
128
- try:
129
- err = r.json().get("error", r.text[:200])
130
- except Exception:
131
- err = r.text[:200]
132
- return {
133
- "ok": False, "error": f"HF API {r.status_code}: {err}",
134
- "hint": "Aggiungi HF_TOKEN nelle variabili d'ambiente per piΓΉ richieste/ora.",
135
- }
136
-
137
- except httpx.TimeoutException:
138
- return {"ok": False, "error": "Timeout 90s β€” modello in cold-start. Riprova tra 30s."}
139
- except Exception as e:
140
- return {"ok": False, "error": str(e)[:300]}
141
-
142
- return {"ok": False, "error": "Impossibile generare dopo 2 tentativi."}
143
 
144
 
145
  # ─── /edit ──────────────────────────────────────────��─────────────────────────
 
18
  3. HF BLIP VQA + captioning (richiede solo HF_TOKEN)
19
 
20
  Image generation and editing use exclusively Hugging Face Inference API:
21
+ - Stable Diffusion 3 Medium for text-to-image generation via HF Inference Providers
22
  - FLUX.1-Kontext-dev for prompt-guided image editing
23
  """
24
  import asyncio, base64, io, os, httpx, logging
 
35
  _USER_AGENT = "Mozilla/5.0 (compatible; AgenteAI/3.0)"
36
 
37
  _MODEL_MAP: dict[str, str] = {
38
+ "FLUX.1-schnell": "stabilityai/stable-diffusion-3-medium-diffusers",
39
+ "FLUX.1-dev": "stabilityai/stable-diffusion-3-medium-diffusers",
40
+ "sdxl": "stabilityai/stable-diffusion-3-medium-diffusers",
41
+ "flux": "stabilityai/stable-diffusion-3-medium-diffusers",
42
+ "flux-schnell": "stabilityai/stable-diffusion-3-medium-diffusers",
43
  }
44
 
45
  _EDIT_MODEL = "timbrooks/instruct-pix2pix"
 
93
  - HF restituisce raw bytes PNG β€” non JSON.
94
  - steps ottimali FLUX.1-schnell: 4 (veloce) – 8 (qualitΓ ).
95
  """
96
+ model_id = _MODEL_MAP.get(req.model, "stabilityai/stable-diffusion-3-medium-diffusers")
97
+ prompt = req.prompt.strip()[:400]
98
+ steps = min(max(req.steps, 1), 8)
99
+ width = min(max(req.width, 256), 1024)
100
+ height = min(max(req.height, 256), 1024)
101
+
102
+ def _run_generation():
103
+ client = InferenceClient(token=os.getenv("HF_TOKEN"), provider="auto", timeout=90)
104
+ return client.text_to_image(
105
+ prompt=prompt,
106
+ model=model_id,
107
+ negative_prompt=req.negative_prompt[:200] if req.negative_prompt else None,
108
+ num_inference_steps=steps,
109
+ width=width,
110
+ height=height,
111
+ )
112
 
113
+ try:
114
+ generated = await asyncio.to_thread(_run_generation)
115
+ output = io.BytesIO()
116
+ generated.save(output, format="PNG")
117
+ return {"ok": True, "image_b64": base64.b64encode(output.getvalue()).decode(), "mime": "image/png", "model": model_id, "prompt": req.prompt[:100]}
118
+ except TimeoutError:
119
+ return {"ok": False, "error": "Timeout 90s β€” modello HF in cold-start. Riprova tra 30s."}
120
+ except Exception as e:
121
+ _logger.warning("HF image generation failed: %s", type(e).__name__)
122
+ return {"ok": False, "error": f"HF image generation unavailable: {str(e)[:300]}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
 
125
  # ─── /edit ──────────────────────────────────────────��─────────────────────────
tests/test_vision_hf_only.py CHANGED
@@ -63,16 +63,21 @@ def test_analyze_uses_hf_vqa_without_openai(monkeypatch):
63
 
64
 
65
  def test_generate_uses_huggingface(monkeypatch):
66
- FakeClient.calls = []
67
- FakeClient.responses = [FakeResponse(payload=None, content=b"generated-png")]
68
- monkeypatch.setattr(vision.httpx, "AsyncClient", FakeClient)
 
 
 
 
 
 
69
 
70
  result = asyncio.run(vision.generate_image(vision.GenerateImageRequest(prompt="un paesaggio")))
71
 
72
  assert result["ok"] is True
73
  assert result["mime"] == "image/png"
74
- assert result["image_b64"] == base64.b64encode(b"generated-png").decode()
75
- assert "router.huggingface.co/hf-inference" in FakeClient.calls[0][0]
76
 
77
 
78
  def test_edit_uses_inference_client_hf(monkeypatch):
 
63
 
64
 
65
  def test_generate_uses_huggingface(monkeypatch):
66
+ class FakeInferenceClient:
67
+ def __init__(self, **kwargs):
68
+ self.kwargs = kwargs
69
+
70
+ def text_to_image(self, **kwargs):
71
+ assert kwargs["model"] == "stabilityai/stable-diffusion-3-medium-diffusers"
72
+ return Image.new("RGB", (1, 1), (0, 120, 255))
73
+
74
+ monkeypatch.setattr(vision, "InferenceClient", FakeInferenceClient)
75
 
76
  result = asyncio.run(vision.generate_image(vision.GenerateImageRequest(prompt="un paesaggio")))
77
 
78
  assert result["ok"] is True
79
  assert result["mime"] == "image/png"
80
+ assert len(base64.b64decode(result["image_b64"])) > 0
 
81
 
82
 
83
  def test_edit_uses_inference_client_hf(monkeypatch):