Spaces:
Sleeping
Sleeping
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>
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,
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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)
|