prashantAI Claude Opus 4.8 (1M context) commited on
Commit
c2cb46f
·
1 Parent(s): fcf8063

Fix invisible hero title + add text-free atmospheric images to results

Browse files

- Hero title rendered transparent (background-clip:text didn't paint);
add display:inline-block and a solid color fallback
- Generate one cinematic, explicitly text-free FLUX image per result,
embedded as a banner in the timeline/world fork card via base64 data URI
- Graceful: cards render fine if image generation is unavailable
- README updated to reflect atmospheric (no-text) imagery

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. README.md +6 -3
  2. app.py +68 -5
README.md CHANGED
@@ -29,9 +29,12 @@ One shared reasoning engine, two lenses on the lives and worlds we didn't live.
29
  - **Reasoning:** [`Qwen/Qwen2.5-7B-Instruct`](https://huggingface.co/Qwen/Qwen2.5-7B-Instruct)
30
  — open weights, **well under the 32B cap**, served via the HF Inference API
31
  (`chat_completion`). Configurable through the `MODEL_NAME_TEXT` env var.
32
- - **Rendering:** the model returns structured JSON, which the app renders as clean,
33
- legible HTML timeline/world cards (no generated-image artifacts diffusion
34
- models can't render readable text, so we render the structure directly).
 
 
 
35
  - **No GPU required to run** — all inference goes through the Inference API, so the
36
  Space stays light and within Zero GPU limits.
37
 
 
29
  - **Reasoning:** [`Qwen/Qwen2.5-7B-Instruct`](https://huggingface.co/Qwen/Qwen2.5-7B-Instruct)
30
  — open weights, **well under the 32B cap**, served via the HF Inference API
31
  (`chat_completion`). Configurable through the `MODEL_NAME_TEXT` env var.
32
+ - **Rendering:** the model returns structured JSON, rendered as an animated HTML
33
+ timeline (glowing year-node spine, branch cards, reflection prompts).
34
+ - **Atmosphere:** each result is topped with a **text-free** cinematic image
35
+ ([`black-forest-labs/FLUX.1-schnell`](https://huggingface.co/black-forest-labs/FLUX.1-schnell)
36
+ via `text_to_image`). Prompts explicitly forbid lettering, so no garbled "text"
37
+ artifacts — just mood. Degrades gracefully if image generation is unavailable.
38
  - **No GPU required to run** — all inference goes through the Inference API, so the
39
  Space stays light and within Zero GPU limits.
40
 
app.py CHANGED
@@ -5,16 +5,19 @@ Uses Qwen2.5-7B-Instruct (via the Hugging Face Inference API) for reasoning, and
5
  renders results as structured, legible HTML cards.
6
  """
7
 
 
8
  import html
9
  import json
10
  import os
11
- from typing import Optional, Dict
 
12
 
13
  import gradio as gr
14
 
15
  import config
16
  import prompts
17
  from model_client import infer_text, infer_json
 
18
 
19
 
20
  # ============================================================================
@@ -210,6 +213,49 @@ def generate_world_divergence(scenario: str) -> Dict:
210
  # HTML Card Rendering
211
  # ============================================================================
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  def _list_items(items) -> str:
214
  """Render a list of strings as escaped <li> elements."""
215
  return "".join(f"<li>{html.escape(str(i))}</li>" for i in items if str(i).strip())
@@ -228,15 +274,17 @@ def _check_items(items) -> str:
228
  return "".join(f"<li>{html.escape(str(i))}</li>" for i in items if str(i).strip())
229
 
230
 
231
- def render_timeline_html(timeline: Dict, direction: str) -> str:
232
  """Render a personal counterfactual timeline as structured cards."""
233
  is_up = direction.lower() == "upward"
234
  arrow = "↑" if is_up else "↓"
235
  label = "The path where it went better" if is_up else "The path where it went harder"
236
  ret = timeline.get("return", {}) or {}
 
237
  return f"""
238
  <div class="rd-stack">
239
  <div class="rd-card rd-fork">
 
240
  <div class="rd-badge">{arrow}</div>
241
  <div class="rd-eyebrow">{label}</div>
242
  <h2 class="rd-title">{html.escape(timeline.get('title', 'Alternate Timeline'))}</h2>
@@ -266,11 +314,13 @@ def render_timeline_html(timeline: Dict, direction: str) -> str:
266
  """
267
 
268
 
269
- def render_world_html(world: Dict) -> str:
270
  """Render an alternate-world divergence as structured cards."""
 
271
  return f"""
272
  <div class="rd-stack">
273
  <div class="rd-card rd-fork">
 
274
  <div class="rd-badge">🌍</div>
275
  <div class="rd-eyebrow">Year {html.escape(str(world.get('year_divergence', '')))} · Point of divergence</div>
276
  <h2 class="rd-title">{html.escape(world.get('title', 'Alternate World'))}</h2>
@@ -332,14 +382,17 @@ body, .gradio-container {
332
 
333
  /* Headers */
334
  .hero-title {
 
335
  font-size: 3.5rem;
336
  font-weight: 800;
 
337
  background: linear-gradient(110deg, var(--accent-blue), var(--accent-purple), #F0ABFC, var(--accent-blue));
338
  background-size: 250% auto;
339
  -webkit-background-clip: text;
340
  background-clip: text;
341
  -webkit-text-fill-color: transparent;
342
  letter-spacing: -1px;
 
343
  animation: rdHero 7s ease infinite;
344
  }
345
  @keyframes rdHero {
@@ -487,6 +540,13 @@ body, .gradio-container {
487
  rgba(139, 92, 246, 0.06);
488
  }
489
 
 
 
 
 
 
 
 
490
  .rd-badge {
491
  display: inline-flex; align-items: center; justify-content: center;
492
  width: 46px; height: 46px; border-radius: 50%;
@@ -665,7 +725,9 @@ def handle_choose_path(direction: str, story: str, analysis: Dict):
665
  return gr.update(value=msg, visible=True), gr.update(visible=False)
666
 
667
  timeline = generate_timeline(story, analysis or FALLBACK_ANALYSIS, direction)
668
- return gr.update(value=render_timeline_html(timeline, direction), visible=True), gr.update(visible=True)
 
 
669
 
670
 
671
  def handle_world_divergence(scenario: str):
@@ -674,7 +736,8 @@ def handle_world_divergence(scenario: str):
674
  return "<div class='rd-card'>Describe a divergence point in at least 20 characters.</div>"
675
 
676
  world = generate_world_divergence(scenario)
677
- return render_world_html(world)
 
678
 
679
 
680
  # Wire events (re-enter the Blocks context so handlers attach correctly)
 
5
  renders results as structured, legible HTML cards.
6
  """
7
 
8
+ import base64
9
  import html
10
  import json
11
  import os
12
+ from io import BytesIO
13
+ from typing import Dict, Optional
14
 
15
  import gradio as gr
16
 
17
  import config
18
  import prompts
19
  from model_client import infer_text, infer_json
20
+ from image_client import get_image_client
21
 
22
 
23
  # ============================================================================
 
213
  # HTML Card Rendering
214
  # ============================================================================
215
 
216
+ # Strongly suppress any lettering — diffusion models render text as gibberish.
217
+ _NO_TEXT = "no text, no words, no letters, no typography, no captions, no signage"
218
+
219
+
220
+ def scene_data_uri(prompt: str) -> str:
221
+ """Generate a text-free atmospheric image and return it as a base64 data URI.
222
+
223
+ Returns "" on any failure so the cards still render without an image.
224
+ """
225
+ if not config.ENABLE_IMAGE_GEN:
226
+ return ""
227
+ try:
228
+ img = get_image_client().generate(prompt)
229
+ if img is None:
230
+ return ""
231
+ buf = BytesIO()
232
+ img.convert("RGB").save(buf, format="JPEG", quality=85)
233
+ b64 = base64.b64encode(buf.getvalue()).decode("ascii")
234
+ return f"data:image/jpeg;base64,{b64}"
235
+ except Exception as e:
236
+ print(f"[warn]scene image failed: {e}")
237
+ return ""
238
+
239
+
240
+ def timeline_scene_prompt(timeline: Dict, direction: str) -> str:
241
+ """Build a text-free image prompt for a personal timeline."""
242
+ mood = "warm golden hopeful luminous sunrise" if direction.lower() == "upward" else "moody cool somber dramatic dusk"
243
+ title = timeline.get("title", "an alternate life")
244
+ return (
245
+ f"Atmospheric cinematic symbolic landscape representing the theme '{title}', "
246
+ f"{mood}, dreamlike concept art, soft volumetric light, painterly, {_NO_TEXT}"
247
+ )
248
+
249
+
250
+ def world_scene_prompt(world: Dict) -> str:
251
+ """Build a text-free image prompt for an alternate world."""
252
+ title = world.get("title", "an alternate world")
253
+ return (
254
+ f"Epic cinematic establishing shot of an alternate world: '{title}', "
255
+ f"atmospheric concept art, dramatic lighting, highly detailed, {_NO_TEXT}"
256
+ )
257
+
258
+
259
  def _list_items(items) -> str:
260
  """Render a list of strings as escaped <li> elements."""
261
  return "".join(f"<li>{html.escape(str(i))}</li>" for i in items if str(i).strip())
 
274
  return "".join(f"<li>{html.escape(str(i))}</li>" for i in items if str(i).strip())
275
 
276
 
277
+ def render_timeline_html(timeline: Dict, direction: str, image_uri: str = "") -> str:
278
  """Render a personal counterfactual timeline as structured cards."""
279
  is_up = direction.lower() == "upward"
280
  arrow = "↑" if is_up else "↓"
281
  label = "The path where it went better" if is_up else "The path where it went harder"
282
  ret = timeline.get("return", {}) or {}
283
+ img = f"<img class='rd-hero-img' src='{image_uri}' alt=''>" if image_uri else ""
284
  return f"""
285
  <div class="rd-stack">
286
  <div class="rd-card rd-fork">
287
+ {img}
288
  <div class="rd-badge">{arrow}</div>
289
  <div class="rd-eyebrow">{label}</div>
290
  <h2 class="rd-title">{html.escape(timeline.get('title', 'Alternate Timeline'))}</h2>
 
314
  """
315
 
316
 
317
+ def render_world_html(world: Dict, image_uri: str = "") -> str:
318
  """Render an alternate-world divergence as structured cards."""
319
+ img = f"<img class='rd-hero-img' src='{image_uri}' alt=''>" if image_uri else ""
320
  return f"""
321
  <div class="rd-stack">
322
  <div class="rd-card rd-fork">
323
+ {img}
324
  <div class="rd-badge">🌍</div>
325
  <div class="rd-eyebrow">Year {html.escape(str(world.get('year_divergence', '')))} · Point of divergence</div>
326
  <h2 class="rd-title">{html.escape(world.get('title', 'Alternate World'))}</h2>
 
382
 
383
  /* Headers */
384
  .hero-title {
385
+ display: inline-block;
386
  font-size: 3.5rem;
387
  font-weight: 800;
388
+ color: var(--accent-blue); /* fallback if background-clip:text is unsupported */
389
  background: linear-gradient(110deg, var(--accent-blue), var(--accent-purple), #F0ABFC, var(--accent-blue));
390
  background-size: 250% auto;
391
  -webkit-background-clip: text;
392
  background-clip: text;
393
  -webkit-text-fill-color: transparent;
394
  letter-spacing: -1px;
395
+ margin-top: 0.5rem;
396
  animation: rdHero 7s ease infinite;
397
  }
398
  @keyframes rdHero {
 
540
  rgba(139, 92, 246, 0.06);
541
  }
542
 
543
+ .rd-hero-img {
544
+ display: block; width: 100%; height: 220px; object-fit: cover;
545
+ border-radius: 14px; margin: -2px 0 16px;
546
+ border: 1px solid var(--border);
547
+ box-shadow: 0 8px 30px rgba(5, 8, 22, 0.55);
548
+ }
549
+
550
  .rd-badge {
551
  display: inline-flex; align-items: center; justify-content: center;
552
  width: 46px; height: 46px; border-radius: 50%;
 
725
  return gr.update(value=msg, visible=True), gr.update(visible=False)
726
 
727
  timeline = generate_timeline(story, analysis or FALLBACK_ANALYSIS, direction)
728
+ image_uri = scene_data_uri(timeline_scene_prompt(timeline, direction))
729
+ html_out = render_timeline_html(timeline, direction, image_uri)
730
+ return gr.update(value=html_out, visible=True), gr.update(visible=True)
731
 
732
 
733
  def handle_world_divergence(scenario: str):
 
736
  return "<div class='rd-card'>Describe a divergence point in at least 20 characters.</div>"
737
 
738
  world = generate_world_divergence(scenario)
739
+ image_uri = scene_data_uri(world_scene_prompt(world))
740
+ return render_world_html(world, image_uri)
741
 
742
 
743
  # Wire events (re-enter the Blocks context so handlers attach correctly)