Spaces:
Running
Running
Commit ·
d3ee9ee
1
Parent(s): 2ab4f11
Harden image generation as remote inference calls with provider fallback
Browse filesImages already ran via HF Inference Providers (no GPU in the Space), but a
single provider being unavailable for the user's token would fail the step.
Now try auto routing then fall back across the providers that serve
FLUX.1-schnell (fal-ai, nscale, together, hf-inference, replicate, wavespeed),
one user-token client each, and surface a clear status if all fail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- pipeline/config.py +9 -1
- pipeline/images.py +44 -10
- pipeline/orchestrator.py +7 -2
pipeline/config.py
CHANGED
|
@@ -13,8 +13,16 @@ MODEL_REASONING = os.environ.get("MODEL_REASONING", "openai/gpt-oss-120b")
|
|
| 13 |
# Strong long-form writer for the actual blog post, with a fallback if unavailable.
|
| 14 |
MODEL_WRITER = os.environ.get("MODEL_WRITER", "deepseek-ai/DeepSeek-V3-0324")
|
| 15 |
MODEL_WRITER_FALLBACK = os.environ.get("MODEL_WRITER_FALLBACK", "Qwen/Qwen2.5-72B-Instruct")
|
| 16 |
-
# Text-to-image model (as requested).
|
|
|
|
| 17 |
MODEL_IMAGE = os.environ.get("MODEL_IMAGE", "black-forest-labs/FLUX.1-schnell")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# Vision-language model for captioning generated images.
|
| 19 |
MODEL_VISION = os.environ.get("MODEL_VISION", "Qwen/Qwen2.5-VL-72B-Instruct")
|
| 20 |
|
|
|
|
| 13 |
# Strong long-form writer for the actual blog post, with a fallback if unavailable.
|
| 14 |
MODEL_WRITER = os.environ.get("MODEL_WRITER", "deepseek-ai/DeepSeek-V3-0324")
|
| 15 |
MODEL_WRITER_FALLBACK = os.environ.get("MODEL_WRITER_FALLBACK", "Qwen/Qwen2.5-72B-Instruct")
|
| 16 |
+
# Text-to-image model (as requested). Generated via remote Inference Providers — this
|
| 17 |
+
# Space has no GPU, so images are always produced by serverless inference calls.
|
| 18 |
MODEL_IMAGE = os.environ.get("MODEL_IMAGE", "black-forest-labs/FLUX.1-schnell")
|
| 19 |
+
# Providers that currently serve FLUX.1-schnell, tried in order after auto-routing.
|
| 20 |
+
# One user-token InferenceClient per provider; billing follows the token.
|
| 21 |
+
IMAGE_PROVIDERS = [
|
| 22 |
+
p.strip() for p in os.environ.get(
|
| 23 |
+
"IMAGE_PROVIDERS", "fal-ai,nscale,together,hf-inference,replicate,wavespeed"
|
| 24 |
+
).split(",") if p.strip()
|
| 25 |
+
]
|
| 26 |
# Vision-language model for captioning generated images.
|
| 27 |
MODEL_VISION = os.environ.get("MODEL_VISION", "Qwen/Qwen2.5-VL-72B-Instruct")
|
| 28 |
|
pipeline/images.py
CHANGED
|
@@ -1,8 +1,15 @@
|
|
| 1 |
-
"""Step 6: turn each [IMAGE: ...] marker into a FLUX prompt and render it.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
from pathlib import Path
|
| 5 |
-
from typing import List
|
| 6 |
|
| 7 |
from huggingface_hub import InferenceClient
|
| 8 |
|
|
@@ -33,24 +40,51 @@ def _flux_prompt(client: InferenceClient, topic: str, scene: str) -> str:
|
|
| 33 |
return f"{scene}, editorial photography, clean composition, natural lighting"
|
| 34 |
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
def generate_images(
|
| 37 |
client: InferenceClient,
|
|
|
|
| 38 |
topic: str,
|
| 39 |
scenes: List[str],
|
| 40 |
run_dir: Path,
|
| 41 |
) -> List[dict]:
|
| 42 |
-
"""Render one image per scene. Returns [{scene, prompt, path|None, error?}]."""
|
| 43 |
run_dir.mkdir(parents=True, exist_ok=True)
|
| 44 |
out: List[dict] = []
|
| 45 |
for i, scene in enumerate(scenes):
|
| 46 |
prompt = _flux_prompt(client, topic, scene)
|
| 47 |
item = {"scene": scene, "prompt": prompt, "path": None}
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
| 55 |
out.append(item)
|
| 56 |
return out
|
|
|
|
| 1 |
+
"""Step 6: turn each [IMAGE: ...] marker into a FLUX prompt and render it.
|
| 2 |
+
|
| 3 |
+
Images are produced entirely by **remote HF Inference Provider calls** (FLUX.1-schnell)
|
| 4 |
+
billed to the user's token — this Space has no GPU, so nothing is generated locally.
|
| 5 |
+
The render tries auto provider routing first, then falls back across the providers that
|
| 6 |
+
serve the model, so a single provider being unavailable for the user's token doesn't
|
| 7 |
+
break the run.
|
| 8 |
+
"""
|
| 9 |
from __future__ import annotations
|
| 10 |
|
| 11 |
from pathlib import Path
|
| 12 |
+
from typing import List, Optional, Tuple
|
| 13 |
|
| 14 |
from huggingface_hub import InferenceClient
|
| 15 |
|
|
|
|
| 40 |
return f"{scene}, editorial photography, clean composition, natural lighting"
|
| 41 |
|
| 42 |
|
| 43 |
+
def _render(hf_token: str, prompt: str) -> Tuple[Optional[object], Optional[str]]:
|
| 44 |
+
"""Generate one image via Inference Providers, trying auto then explicit providers.
|
| 45 |
+
|
| 46 |
+
Returns (PIL image, None) on success or (None, error message) if every provider fails.
|
| 47 |
+
"""
|
| 48 |
+
# None => let the router auto-select; then try each known provider explicitly.
|
| 49 |
+
attempts: List[Optional[str]] = [None] + config.IMAGE_PROVIDERS
|
| 50 |
+
errors: List[str] = []
|
| 51 |
+
for provider in attempts:
|
| 52 |
+
try:
|
| 53 |
+
client = (
|
| 54 |
+
InferenceClient(token=hf_token, provider=provider)
|
| 55 |
+
if provider
|
| 56 |
+
else InferenceClient(token=hf_token)
|
| 57 |
+
)
|
| 58 |
+
image = client.text_to_image(prompt=prompt, model=config.MODEL_IMAGE)
|
| 59 |
+
return image, None
|
| 60 |
+
except Exception as e: # noqa: BLE001 - try the next provider
|
| 61 |
+
errors.append(f"{provider or 'auto'}: {e}")
|
| 62 |
+
continue
|
| 63 |
+
return None, " | ".join(errors[-3:])
|
| 64 |
+
|
| 65 |
+
|
| 66 |
def generate_images(
|
| 67 |
client: InferenceClient,
|
| 68 |
+
hf_token: str,
|
| 69 |
topic: str,
|
| 70 |
scenes: List[str],
|
| 71 |
run_dir: Path,
|
| 72 |
) -> List[dict]:
|
| 73 |
+
"""Render one image per scene via inference calls. Returns [{scene, prompt, path|None, error?}]."""
|
| 74 |
run_dir.mkdir(parents=True, exist_ok=True)
|
| 75 |
out: List[dict] = []
|
| 76 |
for i, scene in enumerate(scenes):
|
| 77 |
prompt = _flux_prompt(client, topic, scene)
|
| 78 |
item = {"scene": scene, "prompt": prompt, "path": None}
|
| 79 |
+
image, err = _render(hf_token, prompt)
|
| 80 |
+
if image is not None:
|
| 81 |
+
try:
|
| 82 |
+
path = run_dir / f"image_{i + 1}.png"
|
| 83 |
+
image.save(path)
|
| 84 |
+
item["path"] = str(path)
|
| 85 |
+
except Exception as e: # noqa: BLE001
|
| 86 |
+
item["error"] = f"save failed: {e}"
|
| 87 |
+
else:
|
| 88 |
+
item["error"] = err or "image generation failed"
|
| 89 |
out.append(item)
|
| 90 |
return out
|
pipeline/orchestrator.py
CHANGED
|
@@ -79,11 +79,16 @@ def run(
|
|
| 79 |
(run_dir / "post.md").write_text(markdown, encoding="utf-8")
|
| 80 |
result["markdown"] = markdown
|
| 81 |
|
| 82 |
-
# 6) generate images
|
| 83 |
scenes = writer.parse_image_markers(markdown)
|
| 84 |
yield 0.70, f"Generating {len(scenes)} images with FLUX.1-schnell…", result
|
| 85 |
-
imgs = images.generate_images(client, topic, scenes, run_dir)
|
| 86 |
result["images"] = imgs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
# 7) caption images
|
| 89 |
yield 0.85, "Captioning images…", result
|
|
|
|
| 79 |
(run_dir / "post.md").write_text(markdown, encoding="utf-8")
|
| 80 |
result["markdown"] = markdown
|
| 81 |
|
| 82 |
+
# 6) generate images (remote FLUX.1-schnell inference calls, billed to the user)
|
| 83 |
scenes = writer.parse_image_markers(markdown)
|
| 84 |
yield 0.70, f"Generating {len(scenes)} images with FLUX.1-schnell…", result
|
| 85 |
+
imgs = images.generate_images(client, hf_token, topic, scenes, run_dir)
|
| 86 |
result["images"] = imgs
|
| 87 |
+
n_ok = sum(1 for im in imgs if im.get("path"))
|
| 88 |
+
if n_ok == 0 and imgs:
|
| 89 |
+
yield 0.72, f"⚠ Image generation failed: {imgs[0].get('error', 'unknown error')}", result
|
| 90 |
+
else:
|
| 91 |
+
yield 0.72, f"Generated {n_ok}/{len(imgs)} images.", result
|
| 92 |
|
| 93 |
# 7) caption images
|
| 94 |
yield 0.85, "Captioning images…", result
|