vivekchakraverty Claude Opus 4.8 commited on
Commit
d3ee9ee
·
1 Parent(s): 2ab4f11

Harden image generation as remote inference calls with provider fallback

Browse files

Images 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 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
- try:
49
- image = client.text_to_image(prompt=prompt, model=config.MODEL_IMAGE)
50
- path = run_dir / f"image_{i + 1}.png"
51
- image.save(path)
52
- item["path"] = str(path)
53
- except Exception as e: # noqa: BLE001 - one bad image shouldn't kill the run
54
- item["error"] = str(e)
 
 
 
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