Spaces:
Sleeping
Sleeping
Commit ·
f8c1b4a
1
Parent(s): 652e422
Wire up working Qwen/FLUX inference, fix Gradio 5 build, add safety fallbacks
Browse files- Switch text inference to HF chat_completion (Qwen2.5-7B), images to
text_to_image (FLUX.1-schnell), removing heavy local torch/diffusers stack
- Fix Gradio: wrap tabs in gr.Tabs(), attach events inside Blocks context,
pin to Gradio 5 and reconcile README sdk_version
- Wire real story/analysis state into path buttons; add return handler
- Add .env loading + .env.example; HF_TOKEN via env/Space secret
- Fix Windows UnicodeEncodeError in log prints
- Add submission-ready README (track tags, rationale) and STATUS.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- .env.example +7 -0
- .gitignore +56 -0
- README.md +56 -3
- STATUS.md +60 -0
- app.py +626 -25
- config.py +63 -0
- image_client.py +113 -0
- model_client.py +130 -0
- prompts.py +101 -0
- requirements.txt +4 -0
.env.example
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copy this file to .env and fill in your Hugging Face access token.
|
| 2 |
+
# Create a token at: https://huggingface.co/settings/tokens (a "Read" token is enough)
|
| 3 |
+
HF_TOKEN=hf_your_token_here
|
| 4 |
+
|
| 5 |
+
# Optional model overrides (defaults are sensible):
|
| 6 |
+
# MODEL_NAME_TEXT=Qwen/Qwen2.5-7B-Instruct
|
| 7 |
+
# MODEL_NAME_IMAGE=black-forest-labs/FLUX.1-schnell
|
.gitignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*$py.class
|
| 4 |
+
*.so
|
| 5 |
+
.Python
|
| 6 |
+
build/
|
| 7 |
+
develop-eggs/
|
| 8 |
+
dist/
|
| 9 |
+
downloads/
|
| 10 |
+
eggs/
|
| 11 |
+
.eggs/
|
| 12 |
+
lib/
|
| 13 |
+
lib64/
|
| 14 |
+
parts/
|
| 15 |
+
sdist/
|
| 16 |
+
var/
|
| 17 |
+
wheels/
|
| 18 |
+
*.egg-info/
|
| 19 |
+
.installed.cfg
|
| 20 |
+
*.egg
|
| 21 |
+
|
| 22 |
+
# Virtual environments
|
| 23 |
+
venv/
|
| 24 |
+
env/
|
| 25 |
+
ENV/
|
| 26 |
+
|
| 27 |
+
# IDE
|
| 28 |
+
.vscode/
|
| 29 |
+
.idea/
|
| 30 |
+
*.swp
|
| 31 |
+
*.swo
|
| 32 |
+
*~
|
| 33 |
+
|
| 34 |
+
# Environment variables
|
| 35 |
+
.env
|
| 36 |
+
.env.local
|
| 37 |
+
|
| 38 |
+
# Models and cache
|
| 39 |
+
models/
|
| 40 |
+
*.bin
|
| 41 |
+
*.pt
|
| 42 |
+
*.pth
|
| 43 |
+
cache/
|
| 44 |
+
huggingface/
|
| 45 |
+
|
| 46 |
+
# OS
|
| 47 |
+
.DS_Store
|
| 48 |
+
Thumbs.db
|
| 49 |
+
|
| 50 |
+
# Logs
|
| 51 |
+
*.log
|
| 52 |
+
logs/
|
| 53 |
+
|
| 54 |
+
# Gradio
|
| 55 |
+
flagged/
|
| 56 |
+
.gradio_cached_examples/
|
README.md
CHANGED
|
@@ -4,11 +4,64 @@ emoji: 😻
|
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.50.0
|
| 8 |
+
python_version: '3.11'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
+
tags:
|
| 13 |
+
- build-small
|
| 14 |
+
- backyard-ai
|
| 15 |
+
- thousand-token-wood
|
| 16 |
---
|
| 17 |
|
| 18 |
+
# Reality Divergence + Elsewhere
|
| 19 |
+
|
| 20 |
+
One shared reasoning engine, two lenses on the lives and worlds we didn't live.
|
| 21 |
+
|
| 22 |
+
- **🪞 Elsewhere (Your Timeline)** — a compassionate, safety-aware counterfactual
|
| 23 |
+
reflection on a single personal choice. *(Track: Backyard AI)*
|
| 24 |
+
- **🌍 Reality Divergence (The World)** — speculative alternate history: pin a
|
| 25 |
+
divergence point, reason forward through plausible ripples. *(Track: Thousand Token Wood)*
|
| 26 |
+
|
| 27 |
+
## Model rationale (Build Small)
|
| 28 |
+
|
| 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 |
+
- **Artifacts:** [`black-forest-labs/FLUX.1-schnell`](https://huggingface.co/black-forest-labs/FLUX.1-schnell)
|
| 33 |
+
via the HF Inference API (`text_to_image`), with SDXL as fallback.
|
| 34 |
+
- **No GPU required to run** — all inference goes through the Inference API, so the
|
| 35 |
+
Space stays light and within Zero GPU limits.
|
| 36 |
+
|
| 37 |
+
## Responsible UX
|
| 38 |
+
|
| 39 |
+
- **Elsewhere** runs input validation + risk detection and, on high-risk input,
|
| 40 |
+
shows a safety notice with crisis resources instead of generating a timeline.
|
| 41 |
+
Every timeline ends with an explicit **return-to-actual-life** strengths section.
|
| 42 |
+
- **Reality Divergence** is speculative and constrained to plausible, non-fantasy outcomes.
|
| 43 |
+
- The app **degrades gracefully**: model/timeout/JSON failures fall back to curated
|
| 44 |
+
content so it never crashes.
|
| 45 |
+
|
| 46 |
+
## Run locally
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
pip install -r requirements.txt
|
| 50 |
+
cp .env.example .env # then paste your HF token into .env
|
| 51 |
+
python app.py
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
Get a token at https://huggingface.co/settings/tokens (a *Read* token is enough).
|
| 55 |
+
Without a valid token the app still runs and shows fallback content.
|
| 56 |
+
|
| 57 |
+
## Submission checklist
|
| 58 |
+
|
| 59 |
+
- [x] Gradio Space, model under 32B, open weights
|
| 60 |
+
- [x] Dual-track UX (Backyard AI + Thousand Token Wood)
|
| 61 |
+
- [x] Safety gate + graceful fallback
|
| 62 |
+
- [ ] Demo video link — _TODO_
|
| 63 |
+
- [ ] Social media post link — _TODO_
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
Configuration reference: https://huggingface.co/docs/hub/spaces-config-reference
|
STATUS.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project Status & Hackathon Alignment
|
| 2 |
+
|
| 3 |
+
_Hugging Face "Build Small" hackathon submission. Last updated: 2026-06-14._
|
| 4 |
+
|
| 5 |
+
## Hackathon rules we're targeting
|
| 6 |
+
|
| 7 |
+
- Model **under 32B** params, open weights ✅ (Qwen2.5-7B-Instruct)
|
| 8 |
+
- **Gradio app** Space ✅
|
| 9 |
+
- Demo video ❌ (TODO)
|
| 10 |
+
- Social-media link in README ❌ (TODO)
|
| 11 |
+
- Zero GPU limit ✅ (all inference via HF Inference API — no local GPU)
|
| 12 |
+
- README track/badge tags ✅ (added)
|
| 13 |
+
- ~~Sponsor prize for MiniCPM builds~~ — **dropped** (see decision below)
|
| 14 |
+
|
| 15 |
+
## Key decisions (2026-06-14)
|
| 16 |
+
|
| 17 |
+
1. **Model: Qwen2.5-7B-Instruct, not MiniCPM.** MiniCPM4.1-8B is not reliably
|
| 18 |
+
served on the HF serverless Inference API, and we chose not to run it locally
|
| 19 |
+
on a ZeroGPU Space. We therefore **drop the MiniCPM sponsor-prize claim** and
|
| 20 |
+
compete on the core Build Small tracks. Qwen2.5-7B is open weights and well
|
| 21 |
+
under the 32B cap, so the submission stays valid.
|
| 22 |
+
2. **Gradio 5 (pinned `>=5,<6`).** The original Gradio 4.44 pin broke against the
|
| 23 |
+
current dependency stack; Gradio 5.50 is verified working. README `sdk_version`
|
| 24 |
+
pinned to `5.50.0` to match.
|
| 25 |
+
|
| 26 |
+
## Architecture
|
| 27 |
+
|
| 28 |
+
- One shared text client (`model_client.py`, HF `chat_completion`) with per-tab
|
| 29 |
+
prompt shaping (`prompts.py`).
|
| 30 |
+
- Image artifacts via HF `text_to_image` (`image_client.py`), FLUX.1-schnell →
|
| 31 |
+
SDXL fallback.
|
| 32 |
+
- Graceful fallbacks everywhere — the app never crashes without a token.
|
| 33 |
+
|
| 34 |
+
## Track mapping
|
| 35 |
+
|
| 36 |
+
| Tab | Track | What it does |
|
| 37 |
+
|---|---|---|
|
| 38 |
+
| 🪞 Elsewhere | Backyard AI | Personal counterfactual + safety gate + return-to-reality |
|
| 39 |
+
| 🌍 Reality Divergence | Thousand Token Wood | Speculative alternate history, ripple reasoning |
|
| 40 |
+
|
| 41 |
+
## Phase status
|
| 42 |
+
|
| 43 |
+
| Phase | Status |
|
| 44 |
+
|---|---|
|
| 45 |
+
| 1 Foundation | ✅ Done & verified (boots, HTTP 200) |
|
| 46 |
+
| 2 Model integration | ✅ Done (Qwen via InferenceClient) |
|
| 47 |
+
| 3 UI & flow | ✅ Done & verified |
|
| 48 |
+
| 4 Safety & guardrails | ✅ Present (risk gate + strengths/return) |
|
| 49 |
+
| 5 Robust fallback | ✅ Verified (401 → graceful fallback) |
|
| 50 |
+
| 6 Testing & optimization | ❌ Not started (timings, cold start, concurrency) |
|
| 51 |
+
| 7 Submission prep | ⏳ README done; demo video + social link + deploy TODO |
|
| 52 |
+
| VoxCPM2 narration (optional) | ❌ `ENABLE_NARRATION=False`, not implemented |
|
| 53 |
+
|
| 54 |
+
## Remaining work
|
| 55 |
+
|
| 56 |
+
- [ ] Test live inference end-to-end with a real HF token
|
| 57 |
+
- [ ] Phase 6: measure startup/inference times, cold start, concurrency
|
| 58 |
+
- [ ] Record demo video (both tabs, model responses, safety gate)
|
| 59 |
+
- [ ] Add demo video + social-media links to README
|
| 60 |
+
- [ ] Deploy to HF Space and confirm clean build
|
app.py
CHANGED
|
@@ -1,36 +1,637 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
"""
|
| 24 |
|
| 25 |
-
demo = gr.Interface(
|
| 26 |
-
fn=generate_reality,
|
| 27 |
-
inputs=gr.Textbox(
|
| 28 |
-
label="Enter a What If Scenario",
|
| 29 |
-
placeholder="What if OpenAI never released ChatGPT?"
|
| 30 |
-
),
|
| 31 |
-
outputs=gr.Textbox(label="Generated Reality"),
|
| 32 |
-
title="Reality Divergence",
|
| 33 |
-
description="Explore alternate worlds generated by AI."
|
| 34 |
-
)
|
| 35 |
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Reality Divergence + Elsewhere
|
| 3 |
+
AI-native platform for exploring counterfactual personal and historical timelines.
|
| 4 |
+
Uses Qwen2.5-7B-Instruct for reasoning + FLUX.1-schnell for artifact generation,
|
| 5 |
+
all served through the Hugging Face Inference API.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
from typing import Tuple, Optional, List, Dict, Any
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from io import BytesIO
|
| 13 |
+
|
| 14 |
import gradio as gr
|
| 15 |
+
from PIL import Image
|
| 16 |
+
|
| 17 |
+
import config
|
| 18 |
+
import prompts
|
| 19 |
+
from model_client import infer_text, infer_json
|
| 20 |
+
from image_client import generate_artifact, get_image_client
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# ============================================================================
|
| 24 |
+
# Fallback Data (for offline/demo mode)
|
| 25 |
+
# ============================================================================
|
| 26 |
+
|
| 27 |
+
FALLBACK_ANALYSIS = {
|
| 28 |
+
"emotion_score": 6,
|
| 29 |
+
"regret_score": 7,
|
| 30 |
+
"self_blame_score": 3,
|
| 31 |
+
"category": "Career",
|
| 32 |
+
"risk_level": "low",
|
| 33 |
+
"risk_markers": []
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
FALLBACK_MIRROR = (
|
| 37 |
+
"You're carrying a moment where one choice—perhaps a risk declined or accepted—"
|
| 38 |
+
"changed the shape of what came next. A part of you may still be wondering who you'd have become "
|
| 39 |
+
"if that moment had unfolded differently. That curiosity deserves a careful frame."
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
FALLBACK_TIMELINE = {
|
| 43 |
+
"title": "The Path Not Taken",
|
| 44 |
+
"divergence": "At the moment you made your choice, everything branched.",
|
| 45 |
+
"ripples": [
|
| 46 |
+
"Your confidence would have grown in a different direction.",
|
| 47 |
+
"New relationships might have formed, others would have stayed distant.",
|
| 48 |
+
"Your skills would have developed along an alternate path.",
|
| 49 |
+
"The people around you would have seen a different version of you.",
|
| 50 |
+
"Your understanding of yourself would be fundamentally different."
|
| 51 |
+
],
|
| 52 |
+
"the_you": (
|
| 53 |
+
"You might have become more outwardly bold or more introspective, depending on the path. "
|
| 54 |
+
"But you would have carried different lessons, different wounds, different strengths."
|
| 55 |
+
),
|
| 56 |
+
"return": {
|
| 57 |
+
"opening": "In the life you actually lived, you learned something you could not have learned elsewhere.",
|
| 58 |
+
"strengths": [
|
| 59 |
+
"You learned to make choices under incomplete information.",
|
| 60 |
+
"You carried responsibility even when the answer was unclear.",
|
| 61 |
+
"You kept enough curiosity to revisit this moment with honesty."
|
| 62 |
+
],
|
| 63 |
+
"lesson": "A path can be meaningful without being the only path that could have mattered.",
|
| 64 |
+
"reflection_question": "What did your actual timeline make possible that the alternate one might have cost?"
|
| 65 |
+
},
|
| 66 |
+
"quote": "Another life may have opened doors, but this one taught you how to stand."
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
FALLBACK_WORLD_DIVERGENCE = {
|
| 70 |
+
"title": "Apple Never Launches the iPhone",
|
| 71 |
+
"divergence_point": "2007: Apple decides to focus on computers instead of mobile",
|
| 72 |
+
"year_divergence": 2007,
|
| 73 |
+
"ripples": [
|
| 74 |
+
"Business phones and BlackBerrys dominate mobile computing.",
|
| 75 |
+
"Apps arrive later and through different distribution channels.",
|
| 76 |
+
"Mobile photography remains less central to daily life.",
|
| 77 |
+
"Touchscreen adoption slows without the iPhone's refinement.",
|
| 78 |
+
"Tech culture evolves around portable computing differently."
|
| 79 |
+
],
|
| 80 |
+
"world_outcome": (
|
| 81 |
+
"A world where mobile computing exists but remains fragmented. "
|
| 82 |
+
"Productivity-focused devices outnumber entertainment devices. "
|
| 83 |
+
"The social media landscape never reaches its current scale."
|
| 84 |
+
),
|
| 85 |
+
"economy": "Business hardware companies thrive. Software consolidation never happens.",
|
| 86 |
+
"culture": "Always-connected culture develops slower. Social media remains desktop-first.",
|
| 87 |
+
"technology": "Cloud services evolve differently. AR/VR developments diverge.",
|
| 88 |
+
"winners": ["Microsoft", "BlackBerry", "Desktop computing"],
|
| 89 |
+
"losers": ["App-based startups", "Mobile creators", "Social media explosion"],
|
| 90 |
+
"reflection": "One device shaped a culture. What else in our world hinges on a single innovation?"
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# ============================================================================
|
| 95 |
+
# Utility Functions
|
| 96 |
+
# ============================================================================
|
| 97 |
+
|
| 98 |
+
def safe_int(val, default=0, min_val=0, max_val=10):
|
| 99 |
+
"""Safely convert and clamp integer values."""
|
| 100 |
+
try:
|
| 101 |
+
return max(min_val, min(max_val, int(val)))
|
| 102 |
+
except (ValueError, TypeError):
|
| 103 |
+
return default
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def safe_str(val, default=""):
|
| 107 |
+
"""Safely convert to string."""
|
| 108 |
+
try:
|
| 109 |
+
return str(val).strip() if val else default
|
| 110 |
+
except Exception:
|
| 111 |
+
return default
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def safe_list(val, default=None):
|
| 115 |
+
"""Safely extract list values."""
|
| 116 |
+
try:
|
| 117 |
+
if isinstance(val, list):
|
| 118 |
+
return [safe_str(v) for v in val]
|
| 119 |
+
return default or []
|
| 120 |
+
except Exception:
|
| 121 |
+
return default or []
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def merge_analysis_with_fallback(parsed: Optional[Dict]) -> Dict:
|
| 125 |
+
"""Merge parsed analysis with fallback; one-way upgrade (risk only escalates)."""
|
| 126 |
+
if not parsed:
|
| 127 |
+
return FALLBACK_ANALYSIS
|
| 128 |
+
|
| 129 |
+
fallback = FALLBACK_ANALYSIS.copy()
|
| 130 |
+
|
| 131 |
+
# Merge, preferring parsed values where valid
|
| 132 |
+
result = {
|
| 133 |
+
"emotion_score": safe_int(parsed.get("emotion_score"), fallback["emotion_score"]),
|
| 134 |
+
"regret_score": safe_int(parsed.get("regret_score"), fallback["regret_score"]),
|
| 135 |
+
"self_blame_score": safe_int(parsed.get("self_blame_score"), fallback["self_blame_score"]),
|
| 136 |
+
"category": safe_str(parsed.get("category"), fallback["category"]),
|
| 137 |
+
"risk_level": "high" if (parsed.get("risk_level") == "high" or fallback["risk_level"] == "high") else safe_str(parsed.get("risk_level"), fallback["risk_level"]),
|
| 138 |
+
"risk_markers": safe_list(parsed.get("risk_markers"), fallback["risk_markers"])
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
return result
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# ============================================================================
|
| 145 |
+
# Safety & Analysis
|
| 146 |
+
# ============================================================================
|
| 147 |
+
|
| 148 |
+
def analyze_story(story: str) -> Dict:
|
| 149 |
+
"""Analyze personal story for emotion, regret, and risk."""
|
| 150 |
+
if not story or len(story.strip()) < config.MIN_STORY_LENGTH:
|
| 151 |
+
return FALLBACK_ANALYSIS
|
| 152 |
+
|
| 153 |
+
# Try model analysis
|
| 154 |
+
parsed = infer_json(prompts.ANALYSIS_PROMPT, story, temperature=0.1, max_tokens=1024)
|
| 155 |
+
return merge_analysis_with_fallback(parsed)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def generate_mirror(story: str, analysis: Dict) -> str:
|
| 159 |
+
"""Generate compassionate mirror reflection."""
|
| 160 |
+
prompt = f"Story:\n{story}\n\nAnalysis:\n{json.dumps(analysis)}"
|
| 161 |
+
mirror = infer_text(prompts.MIRROR_PROMPT, prompt, temperature=0.55, max_tokens=150)
|
| 162 |
+
return mirror if mirror else FALLBACK_MIRROR
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def generate_timeline(story: str, analysis: Dict, direction: str) -> Dict:
|
| 166 |
+
"""Generate counterfactual timeline (Upward/Downward)."""
|
| 167 |
+
prompt = f"""Story:\n{story}\n\nAnalysis:\n{json.dumps(analysis)}\n\nDirection: {direction}"""
|
| 168 |
+
parsed = infer_json(prompts.TIMELINE_PROMPT, prompt, temperature=0.7, max_tokens=1024)
|
| 169 |
+
|
| 170 |
+
if not parsed:
|
| 171 |
+
return FALLBACK_TIMELINE
|
| 172 |
+
|
| 173 |
+
# Validate structure
|
| 174 |
+
return {
|
| 175 |
+
"title": safe_str(parsed.get("title"), FALLBACK_TIMELINE["title"]),
|
| 176 |
+
"divergence": safe_str(parsed.get("divergence"), FALLBACK_TIMELINE["divergence"]),
|
| 177 |
+
"ripples": safe_list(parsed.get("ripples"), FALLBACK_TIMELINE["ripples"]),
|
| 178 |
+
"the_you": safe_str(parsed.get("the_you"), FALLBACK_TIMELINE["the_you"]),
|
| 179 |
+
"return": {
|
| 180 |
+
"opening": safe_str(parsed.get("return", {}).get("opening"), FALLBACK_TIMELINE["return"]["opening"]),
|
| 181 |
+
"strengths": safe_list(parsed.get("return", {}).get("strengths"), FALLBACK_TIMELINE["return"]["strengths"]),
|
| 182 |
+
"lesson": safe_str(parsed.get("return", {}).get("lesson"), FALLBACK_TIMELINE["return"]["lesson"]),
|
| 183 |
+
"reflection_question": safe_str(parsed.get("return", {}).get("reflection_question"), FALLBACK_TIMELINE["return"]["reflection_question"])
|
| 184 |
+
},
|
| 185 |
+
"quote": safe_str(parsed.get("quote"), FALLBACK_TIMELINE["quote"])
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def generate_world_divergence(scenario: str) -> Dict:
|
| 190 |
+
"""Generate world/historical divergence."""
|
| 191 |
+
prompt = f"Divergence scenario: {scenario}"
|
| 192 |
+
parsed = infer_json(prompts.WORLD_DIVERGENCE_PROMPT, prompt, temperature=0.7, max_tokens=1024)
|
| 193 |
+
|
| 194 |
+
if not parsed:
|
| 195 |
+
return FALLBACK_WORLD_DIVERGENCE
|
| 196 |
+
|
| 197 |
+
return {
|
| 198 |
+
"title": safe_str(parsed.get("title"), FALLBACK_WORLD_DIVERGENCE["title"]),
|
| 199 |
+
"divergence_point": safe_str(parsed.get("divergence_point"), FALLBACK_WORLD_DIVERGENCE["divergence_point"]),
|
| 200 |
+
"year_divergence": safe_int(parsed.get("year_divergence"), 2000, 1800, 2050),
|
| 201 |
+
"ripples": safe_list(parsed.get("ripples"), FALLBACK_WORLD_DIVERGENCE["ripples"]),
|
| 202 |
+
"world_outcome": safe_str(parsed.get("world_outcome"), FALLBACK_WORLD_DIVERGENCE["world_outcome"]),
|
| 203 |
+
"economy": safe_str(parsed.get("economy"), FALLBACK_WORLD_DIVERGENCE["economy"]),
|
| 204 |
+
"culture": safe_str(parsed.get("culture"), FALLBACK_WORLD_DIVERGENCE["culture"]),
|
| 205 |
+
"technology": safe_str(parsed.get("technology"), FALLBACK_WORLD_DIVERGENCE["technology"]),
|
| 206 |
+
"winners": safe_list(parsed.get("winners"), FALLBACK_WORLD_DIVERGENCE["winners"]),
|
| 207 |
+
"losers": safe_list(parsed.get("losers"), FALLBACK_WORLD_DIVERGENCE["losers"]),
|
| 208 |
+
"reflection": safe_str(parsed.get("reflection"), FALLBACK_WORLD_DIVERGENCE["reflection"])
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# ============================================================================
|
| 213 |
+
# Artifact Generation
|
| 214 |
+
# ============================================================================
|
| 215 |
+
|
| 216 |
+
def generate_artifacts(timeline_data: Dict, num_artifacts: int = 3) -> List[Optional[Image.Image]]:
|
| 217 |
+
"""Generate 3-6 artifact images for timeline."""
|
| 218 |
+
artifacts = []
|
| 219 |
+
|
| 220 |
+
try:
|
| 221 |
+
# Newspaper
|
| 222 |
+
headline = timeline_data.get("title", "Alternate Reality")
|
| 223 |
+
artifacts.append(generate_artifact("newspaper", headline=headline, subheading=timeline_data.get("divergence", "")))
|
| 224 |
+
except Exception as e:
|
| 225 |
+
print(f"[warn]Newspaper artifact failed: {e}")
|
| 226 |
+
artifacts.append(None)
|
| 227 |
+
|
| 228 |
+
try:
|
| 229 |
+
# Product/Ad
|
| 230 |
+
artifacts.append(generate_artifact("product", product="Alternate World Artifact", context=timeline_data.get("divergence", "")))
|
| 231 |
+
except Exception as e:
|
| 232 |
+
print(f"[warn]Product artifact failed: {e}")
|
| 233 |
+
artifacts.append(None)
|
| 234 |
+
|
| 235 |
+
try:
|
| 236 |
+
# Document
|
| 237 |
+
artifacts.append(generate_artifact("document", doc_type="diary entry", context=timeline_data.get("divergence", "")))
|
| 238 |
+
except Exception as e:
|
| 239 |
+
print(f"[warn]Document artifact failed: {e}")
|
| 240 |
+
artifacts.append(None)
|
| 241 |
+
|
| 242 |
+
return [a for a in artifacts if a is not None]
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# ============================================================================
|
| 246 |
+
# CSS Styling (SVG-inspired dark theme)
|
| 247 |
+
# ============================================================================
|
| 248 |
+
|
| 249 |
+
CSS = """
|
| 250 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
| 251 |
+
|
| 252 |
+
:root {
|
| 253 |
+
--bg-dark: #050816;
|
| 254 |
+
--bg-mid: #08111F;
|
| 255 |
+
--bg-light: #130A24;
|
| 256 |
+
--accent-blue: #38BDF8;
|
| 257 |
+
--accent-purple: #8B5CF6;
|
| 258 |
+
--text-primary: #F8FAFC;
|
| 259 |
+
--text-secondary: #94A3B8;
|
| 260 |
+
--border: #334155;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
body, .gradio-container {
|
| 264 |
+
background: linear-gradient(135deg, var(--bg-dark), var(--bg-mid), var(--bg-light));
|
| 265 |
+
color: var(--text-primary);
|
| 266 |
+
font-family: Inter, system-ui, sans-serif;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.gradio-container {
|
| 270 |
+
max-width: 1400px;
|
| 271 |
+
margin: auto;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
/* Headers */
|
| 275 |
+
.hero-title {
|
| 276 |
+
font-size: 3.5rem;
|
| 277 |
+
font-weight: 800;
|
| 278 |
+
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
|
| 279 |
+
-webkit-background-clip: text;
|
| 280 |
+
-webkit-text-fill-color: transparent;
|
| 281 |
+
letter-spacing: -1px;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.hero-subtitle {
|
| 285 |
+
font-size: 1.25rem;
|
| 286 |
+
color: var(--text-secondary);
|
| 287 |
+
margin-top: 1rem;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
/* Cards */
|
| 291 |
+
.timeline-card, .artifact-container, .world-card {
|
| 292 |
+
background: linear-gradient(180deg, rgba(17, 24, 39, 0.5), rgba(11, 16, 32, 0.3));
|
| 293 |
+
border: 1px solid var(--border);
|
| 294 |
+
border-radius: 16px;
|
| 295 |
+
padding: 24px;
|
| 296 |
+
backdrop-filter: blur(8px);
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.timeline-card:hover {
|
| 300 |
+
border-color: var(--accent-blue);
|
| 301 |
+
box-shadow: 0 0 20px rgba(56, 189, 248, 0.2);
|
| 302 |
+
}
|
| 303 |
|
| 304 |
+
/* Buttons */
|
| 305 |
+
.gr-button {
|
| 306 |
+
background: var(--accent-blue) !important;
|
| 307 |
+
color: white !important;
|
| 308 |
+
border: 0 !important;
|
| 309 |
+
border-radius: 12px !important;
|
| 310 |
+
font-weight: 700 !important;
|
| 311 |
+
transition: all 0.3s ease;
|
| 312 |
+
}
|
| 313 |
|
| 314 |
+
.gr-button:hover {
|
| 315 |
+
background: var(--accent-purple) !important;
|
| 316 |
+
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
|
| 317 |
+
}
|
| 318 |
|
| 319 |
+
/* Tabs */
|
| 320 |
+
.gr-tabs-nav {
|
| 321 |
+
border-bottom: 2px solid var(--border);
|
| 322 |
+
}
|
| 323 |
|
| 324 |
+
.gr-tabs-nav .gr-tab-nav-item {
|
| 325 |
+
color: var(--text-secondary);
|
| 326 |
+
font-weight: 600;
|
| 327 |
+
}
|
| 328 |
|
| 329 |
+
.gr-tabs-nav .gr-tab-nav-item.gr-tab-nav-item-selected {
|
| 330 |
+
color: var(--accent-blue);
|
| 331 |
+
border-bottom: 3px solid var(--accent-blue);
|
| 332 |
+
}
|
| 333 |
|
| 334 |
+
/* Layout helpers */
|
| 335 |
+
.parallel-layout {
|
| 336 |
+
display: grid;
|
| 337 |
+
grid-template-columns: 1fr 1fr;
|
| 338 |
+
gap: 2rem;
|
| 339 |
+
margin-top: 2rem;
|
| 340 |
+
}
|
| 341 |
|
| 342 |
+
.timeline-column, .image-column {
|
| 343 |
+
display: flex;
|
| 344 |
+
flex-direction: column;
|
| 345 |
+
gap: 1.5rem;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.artifact-grid {
|
| 349 |
+
display: grid;
|
| 350 |
+
grid-template-columns: repeat(2, 1fr);
|
| 351 |
+
gap: 1rem;
|
| 352 |
+
margin-top: 1rem;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.artifact-item {
|
| 356 |
+
border-radius: 12px;
|
| 357 |
+
overflow: hidden;
|
| 358 |
+
border: 1px solid var(--border);
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.artifact-item img {
|
| 362 |
+
width: 100%;
|
| 363 |
+
height: auto;
|
| 364 |
+
display: block;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
/* Mobile */
|
| 368 |
+
@media (max-width: 1024px) {
|
| 369 |
+
.parallel-layout {
|
| 370 |
+
grid-template-columns: 1fr;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.hero-title {
|
| 374 |
+
font-size: 2rem;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.artifact-grid {
|
| 378 |
+
grid-template-columns: 1fr;
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
"""
|
| 382 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
|
| 384 |
+
# ============================================================================
|
| 385 |
+
# Gradio UI
|
| 386 |
+
# ============================================================================
|
| 387 |
+
|
| 388 |
+
with gr.Blocks(css=CSS, title="Reality Divergence") as demo:
|
| 389 |
+
|
| 390 |
+
# State variables
|
| 391 |
+
story_state = gr.State("")
|
| 392 |
+
analysis_state = gr.State(None)
|
| 393 |
+
timeline_state = gr.State(None)
|
| 394 |
+
direction_state = gr.State("Upward")
|
| 395 |
+
|
| 396 |
+
with gr.Tabs():
|
| 397 |
+
# ========== ELSEWHERE TAB ==========
|
| 398 |
+
with gr.TabItem("🪞 Elsewhere (Your Timeline)", id="elsewhere"):
|
| 399 |
+
|
| 400 |
+
# Hero
|
| 401 |
+
gr.HTML("<div class='hero-title'>Explore the Life You Didn't Live</div>")
|
| 402 |
+
gr.HTML("<div class='hero-subtitle'>A compassionate reflection on one choice that changed everything.</div>")
|
| 403 |
+
|
| 404 |
+
# Input
|
| 405 |
+
with gr.Group():
|
| 406 |
+
gr.Markdown("#### Your Moment")
|
| 407 |
+
story_input = gr.Textbox(
|
| 408 |
+
label="Tell me about a decision, event, or conversation that changed the shape of what came next.",
|
| 409 |
+
placeholder="Share what happened, what you chose, and what you wonder about...",
|
| 410 |
+
lines=8,
|
| 411 |
+
max_lines=12
|
| 412 |
+
)
|
| 413 |
+
submit_btn = gr.Button("Reflect", variant="primary", size="lg")
|
| 414 |
+
|
| 415 |
+
# Output: parallel layout
|
| 416 |
+
with gr.Group():
|
| 417 |
+
with gr.Row():
|
| 418 |
+
# Left: narrative
|
| 419 |
+
with gr.Column(scale=1):
|
| 420 |
+
gr.Markdown("#### Your Reflection")
|
| 421 |
+
mirror_output = gr.Markdown(FALLBACK_MIRROR, elem_classes=["timeline-card"])
|
| 422 |
+
|
| 423 |
+
# Path selection
|
| 424 |
+
gr.Markdown("#### Choose a Path")
|
| 425 |
+
with gr.Row():
|
| 426 |
+
upward_btn = gr.Button("↑ What if it went better?", variant="primary")
|
| 427 |
+
downward_btn = gr.Button("↓ What if it went worse?")
|
| 428 |
+
|
| 429 |
+
# Right: potential artifacts
|
| 430 |
+
with gr.Column(scale=1):
|
| 431 |
+
gr.Markdown("#### Artifacts from Alternate Lives")
|
| 432 |
+
artifact_1 = gr.Image(label="Artifact 1", type="pil", scale=1)
|
| 433 |
+
artifact_2 = gr.Image(label="Artifact 2", type="pil", scale=1)
|
| 434 |
+
artifact_3 = gr.Image(label="Artifact 3", type="pil", scale=1)
|
| 435 |
+
|
| 436 |
+
# Full timeline result (hidden until path chosen)
|
| 437 |
+
timeline_result = gr.Markdown(visible=False, elem_classes=["timeline-card"])
|
| 438 |
+
return_button = gr.Button("↩ Return to Your Actual Life", visible=False, variant="primary")
|
| 439 |
+
|
| 440 |
+
# ========== REALITY DIVERGENCE TAB ==========
|
| 441 |
+
with gr.TabItem("🌍 Reality Divergence (The World)", id="reality"):
|
| 442 |
+
|
| 443 |
+
# Hero
|
| 444 |
+
gr.HTML("<div class='hero-title'>The Museum of Worlds That Never Happened</div>")
|
| 445 |
+
gr.HTML("<div class='hero-subtitle'>What if one decision shaped the entire world?</div>")
|
| 446 |
+
|
| 447 |
+
# Input
|
| 448 |
+
with gr.Group():
|
| 449 |
+
gr.Markdown("#### Divergence Point")
|
| 450 |
+
world_input = gr.Textbox(
|
| 451 |
+
label="Describe a historical or future decision that would reshape civilization.",
|
| 452 |
+
placeholder="e.g., 'What if the Renaissance never happened?' or 'What if AI was never invented?'",
|
| 453 |
+
lines=6
|
| 454 |
+
)
|
| 455 |
+
world_submit = gr.Button("Generate World", variant="primary", size="lg")
|
| 456 |
+
|
| 457 |
+
# Output: parallel layout
|
| 458 |
+
with gr.Group():
|
| 459 |
+
with gr.Row():
|
| 460 |
+
# Left: world narrative
|
| 461 |
+
with gr.Column(scale=1):
|
| 462 |
+
gr.Markdown("#### The Alternate World")
|
| 463 |
+
world_title = gr.Markdown("", elem_classes=["world-card"])
|
| 464 |
+
world_divergence = gr.Markdown("", elem_classes=["world-card"])
|
| 465 |
+
world_ripples = gr.Markdown("", elem_classes=["world-card"])
|
| 466 |
+
world_outcome = gr.Markdown("", elem_classes=["world-card"])
|
| 467 |
+
|
| 468 |
+
# Right: world artifacts (museum)
|
| 469 |
+
with gr.Column(scale=1):
|
| 470 |
+
gr.Markdown("#### Museum Artifacts")
|
| 471 |
+
with gr.Row():
|
| 472 |
+
world_artifact_1 = gr.Image(label="Economic Impact", type="pil", scale=1)
|
| 473 |
+
world_artifact_2 = gr.Image(label="Cultural Shift", type="pil", scale=1)
|
| 474 |
+
with gr.Row():
|
| 475 |
+
world_artifact_3 = gr.Image(label="Technology", type="pil", scale=1)
|
| 476 |
+
world_artifact_4 = gr.Image(label="Daily Life", type="pil", scale=1)
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
# ============================================================================
|
| 480 |
+
# Event Handlers
|
| 481 |
+
# ============================================================================
|
| 482 |
+
|
| 483 |
+
def handle_submit_story(story: str):
|
| 484 |
+
"""Process personal story submission.
|
| 485 |
+
|
| 486 |
+
Returns: (mirror_md, artifact_1, artifact_2, artifact_3, story_state, analysis_state)
|
| 487 |
+
"""
|
| 488 |
+
if not story or len(story.strip()) < config.MIN_STORY_LENGTH:
|
| 489 |
+
return "Please share at least 100 characters.", None, None, None, "", None
|
| 490 |
+
|
| 491 |
+
if len(story) > config.MAX_STORY_LENGTH:
|
| 492 |
+
return "Please keep under 5000 characters.", None, None, None, "", None
|
| 493 |
+
|
| 494 |
+
# Analyze
|
| 495 |
+
analysis = analyze_story(story)
|
| 496 |
+
|
| 497 |
+
if analysis.get("risk_level") == "high":
|
| 498 |
+
safety_msg = (
|
| 499 |
+
"### ⚠ Safety Notice\n\n"
|
| 500 |
+
"This memory touches on deep pain. Elsewhere is not designed for trauma or crisis reflection.\n\n"
|
| 501 |
+
"**If you're in danger, reach out:**\n"
|
| 502 |
+
"- US: 988 (Suicide & Crisis Lifeline)\n"
|
| 503 |
+
"- Or contact local emergency services."
|
| 504 |
+
)
|
| 505 |
+
return safety_msg, None, None, None, "", None
|
| 506 |
+
|
| 507 |
+
# Mirror
|
| 508 |
+
mirror = generate_mirror(story, analysis)
|
| 509 |
+
|
| 510 |
+
# Artifacts (gracefully handle if image gen fails)
|
| 511 |
+
try:
|
| 512 |
+
artifacts = generate_artifacts({"title": "Your Alternate Life", "divergence": story})
|
| 513 |
+
artifact_imgs = [artifacts[i] if i < len(artifacts) else None for i in range(3)]
|
| 514 |
+
except Exception as e:
|
| 515 |
+
print(f"[warn]Artifact generation failed: {e}")
|
| 516 |
+
artifact_imgs = [None, None, None]
|
| 517 |
+
|
| 518 |
+
return (
|
| 519 |
+
mirror,
|
| 520 |
+
artifact_imgs[0],
|
| 521 |
+
artifact_imgs[1],
|
| 522 |
+
artifact_imgs[2],
|
| 523 |
+
story,
|
| 524 |
+
analysis,
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
def handle_choose_path(direction: str, story: str, analysis: Dict):
|
| 529 |
+
"""Generate timeline for chosen path."""
|
| 530 |
+
if not story or len(story.strip()) < config.MIN_STORY_LENGTH:
|
| 531 |
+
return gr.update(
|
| 532 |
+
value="Please share your moment above and click **Reflect** first.",
|
| 533 |
+
visible=True,
|
| 534 |
+
), gr.update(visible=False)
|
| 535 |
+
|
| 536 |
+
timeline = generate_timeline(story, analysis or FALLBACK_ANALYSIS, direction)
|
| 537 |
+
|
| 538 |
+
result_md = f"""
|
| 539 |
+
### {direction.upper()} PATH: {timeline.get('title', 'Alternate Timeline')}
|
| 540 |
+
|
| 541 |
+
**Divergence Point:** {timeline.get('divergence', '')}
|
| 542 |
+
|
| 543 |
+
**Ripples:**
|
| 544 |
+
"""
|
| 545 |
+
for ripple in timeline.get('ripples', []):
|
| 546 |
+
result_md += f"\n- {ripple}"
|
| 547 |
+
|
| 548 |
+
result_md += f"\n\n**Who You Might Have Become:** {timeline.get('the_you', '')}"
|
| 549 |
+
|
| 550 |
+
result_md += f"\n\n**The Return**\n\n{timeline.get('return', {}).get('opening', '')}"
|
| 551 |
+
result_md += f"\n\n**Strengths in Your Actual Timeline:**\n"
|
| 552 |
+
for strength in timeline.get('return', {}).get('strengths', []):
|
| 553 |
+
result_md += f"\n- {strength}"
|
| 554 |
+
|
| 555 |
+
result_md += f"\n\n**Lesson:** {timeline.get('return', {}).get('lesson', '')}"
|
| 556 |
+
result_md += f"\n\n**Reflection Question:** {timeline.get('return', {}).get('reflection_question', '')}"
|
| 557 |
+
result_md += f"\n\n> *\"{timeline.get('quote', '')}\"*"
|
| 558 |
+
|
| 559 |
+
return gr.update(value=result_md, visible=True), gr.update(visible=True)
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
def handle_world_divergence(scenario: str):
|
| 563 |
+
"""Generate alternate world."""
|
| 564 |
+
if not scenario or len(scenario.strip()) < 20:
|
| 565 |
+
return "", "", "", "", None, None, None, None
|
| 566 |
+
|
| 567 |
+
world = generate_world_divergence(scenario)
|
| 568 |
+
|
| 569 |
+
title_md = f"## {world.get('title', 'Alternate World')}"
|
| 570 |
+
divergence_md = f"**Year {world.get('year_divergence', 'N/A')}:** {world.get('divergence_point', '')}"
|
| 571 |
+
|
| 572 |
+
ripples_md = "**Ripples:**\n"
|
| 573 |
+
for ripple in world.get('ripples', []):
|
| 574 |
+
ripples_md += f"\n- {ripple}"
|
| 575 |
+
|
| 576 |
+
outcome_md = f"**World Outcome:** {world.get('world_outcome', '')}\n\n"
|
| 577 |
+
outcome_md += f"*Economy:* {world.get('economy', '')}\n\n"
|
| 578 |
+
outcome_md += f"*Culture:* {world.get('culture', '')}\n\n"
|
| 579 |
+
outcome_md += f"*Technology:* {world.get('technology', '')}"
|
| 580 |
+
|
| 581 |
+
# Artifacts
|
| 582 |
+
try:
|
| 583 |
+
artifacts = generate_artifacts(world)
|
| 584 |
+
artifact_imgs = [artifacts[i] if i < len(artifacts) else None for i in range(4)]
|
| 585 |
+
except Exception as e:
|
| 586 |
+
print(f"[warn]Artifact generation failed: {e}")
|
| 587 |
+
artifact_imgs = [None, None, None, None]
|
| 588 |
+
|
| 589 |
+
return (
|
| 590 |
+
title_md,
|
| 591 |
+
divergence_md,
|
| 592 |
+
ripples_md,
|
| 593 |
+
outcome_md,
|
| 594 |
+
artifact_imgs[0] if len(artifact_imgs) > 0 else None,
|
| 595 |
+
artifact_imgs[1] if len(artifact_imgs) > 1 else None,
|
| 596 |
+
artifact_imgs[2] if len(artifact_imgs) > 2 else None,
|
| 597 |
+
artifact_imgs[3] if len(artifact_imgs) > 3 else None,
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
# Wire events (re-enter the Blocks context so handlers attach correctly)
|
| 602 |
+
with demo:
|
| 603 |
+
submit_btn.click(
|
| 604 |
+
fn=handle_submit_story,
|
| 605 |
+
inputs=[story_input],
|
| 606 |
+
outputs=[mirror_output, artifact_1, artifact_2, artifact_3, story_state, analysis_state]
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
upward_btn.click(
|
| 610 |
+
fn=lambda story, analysis: handle_choose_path("Upward", story, analysis),
|
| 611 |
+
inputs=[story_state, analysis_state],
|
| 612 |
+
outputs=[timeline_result, return_button]
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
downward_btn.click(
|
| 616 |
+
fn=lambda story, analysis: handle_choose_path("Downward", story, analysis),
|
| 617 |
+
inputs=[story_state, analysis_state],
|
| 618 |
+
outputs=[timeline_result, return_button]
|
| 619 |
+
)
|
| 620 |
+
|
| 621 |
+
return_button.click(
|
| 622 |
+
fn=lambda: (gr.update(visible=False), gr.update(visible=False)),
|
| 623 |
+
outputs=[timeline_result, return_button]
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
world_submit.click(
|
| 627 |
+
fn=handle_world_divergence,
|
| 628 |
+
inputs=[world_input],
|
| 629 |
+
outputs=[world_title, world_divergence, world_ripples, world_outcome, world_artifact_1, world_artifact_2, world_artifact_3, world_artifact_4]
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
|
| 633 |
+
if __name__ == "__main__":
|
| 634 |
+
demo.launch(
|
| 635 |
+
server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 636 |
+
server_port=int(os.getenv("PORT", os.getenv("GRADIO_SERVER_PORT", "7860"))),
|
| 637 |
+
)
|
config.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration and constants for Reality Divergence app."""
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
try:
|
| 5 |
+
# Load HF_TOKEN (and any other vars) from a local .env file if present.
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
load_dotenv()
|
| 8 |
+
except ImportError:
|
| 9 |
+
pass
|
| 10 |
+
|
| 11 |
+
# App metadata
|
| 12 |
+
APP_NAME = "Reality Divergence"
|
| 13 |
+
TAGLINE = "Explore the lives and worlds you didn't live."
|
| 14 |
+
VERSION = "1.0.0"
|
| 15 |
+
|
| 16 |
+
# Model configuration
|
| 17 |
+
# Hugging Face access token. Set it in a .env file (HF_TOKEN=hf_xxx) or as an
|
| 18 |
+
# environment variable. Get one at https://huggingface.co/settings/tokens
|
| 19 |
+
HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
|
| 20 |
+
|
| 21 |
+
# Text model served through the HF Inference API (chat_completion).
|
| 22 |
+
# Qwen2.5-7B-Instruct: open weights, well under the 32B hackathon cap, reliably
|
| 23 |
+
# available on the Inference API. Override via the MODEL_NAME_TEXT env var.
|
| 24 |
+
MODEL_NAME_TEXT = os.getenv("MODEL_NAME_TEXT", "Qwen/Qwen2.5-7B-Instruct")
|
| 25 |
+
|
| 26 |
+
# Image models served through the HF Inference API (text_to_image).
|
| 27 |
+
MODEL_NAME_IMAGE = os.getenv("MODEL_NAME_IMAGE", "black-forest-labs/FLUX.1-schnell")
|
| 28 |
+
FALLBACK_IMAGE_MODEL = "stabilityai/stable-diffusion-xl-base-1.0"
|
| 29 |
+
|
| 30 |
+
# Inference settings
|
| 31 |
+
TIMEOUT_INFERENCE = 60 # seconds
|
| 32 |
+
TIMEOUT_IMAGE = 45 # seconds
|
| 33 |
+
MAX_TOKENS_TEXT = 512
|
| 34 |
+
MAX_TOKENS_MIRROR = 150
|
| 35 |
+
TEMPERATURE_ANALYSIS = 0.1
|
| 36 |
+
TEMPERATURE_NARRATIVE = 0.7
|
| 37 |
+
TEMPERATURE_IMAGE = 0.8
|
| 38 |
+
|
| 39 |
+
# Safety settings
|
| 40 |
+
MIN_STORY_LENGTH = 100
|
| 41 |
+
MAX_STORY_LENGTH = 5000
|
| 42 |
+
RATE_LIMIT_ELSEWHERE = 5 # requests per minute
|
| 43 |
+
RATE_LIMIT_REALITY = 20
|
| 44 |
+
|
| 45 |
+
# UI/UX
|
| 46 |
+
THEME_BG_DARK = "#050816"
|
| 47 |
+
THEME_BG_MID = "#08111F"
|
| 48 |
+
THEME_BG_LIGHT = "#130A24"
|
| 49 |
+
THEME_ACCENT_BLUE = "#38BDF8"
|
| 50 |
+
THEME_ACCENT_PURPLE = "#8B5CF6"
|
| 51 |
+
THEME_TEXT_PRIMARY = "#F8FAFC"
|
| 52 |
+
THEME_TEXT_SECONDARY = "#94A3B8"
|
| 53 |
+
|
| 54 |
+
# Feature flags
|
| 55 |
+
ENABLE_NARRATION = False # VoxCPM2 optional
|
| 56 |
+
ENABLE_IMAGE_GEN = True
|
| 57 |
+
ENABLE_ARTIFACTS = True
|
| 58 |
+
ENABLE_SHARE_CARD = True
|
| 59 |
+
|
| 60 |
+
# Paths
|
| 61 |
+
ASSETS_PATH = "assets"
|
| 62 |
+
SVG_HOME = f"{ASSETS_PATH}/reality-divergence-home.svg"
|
| 63 |
+
SVG_DASHBOARD = f"{ASSETS_PATH}/reality-divergence-dashboard.svg"
|
image_client.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Image generation client via the Hugging Face Inference API (text_to_image)."""
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from PIL import Image
|
| 4 |
+
|
| 5 |
+
try:
|
| 6 |
+
from huggingface_hub import InferenceClient
|
| 7 |
+
except ImportError:
|
| 8 |
+
InferenceClient = None
|
| 9 |
+
|
| 10 |
+
import config
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ImageGenerationClient:
|
| 14 |
+
"""Generate artifact images for alternate timelines via the HF Inference API."""
|
| 15 |
+
|
| 16 |
+
def __init__(self, hf_token: Optional[str] = None):
|
| 17 |
+
self.hf_token = hf_token or config.HF_TOKEN
|
| 18 |
+
self.model = config.MODEL_NAME_IMAGE
|
| 19 |
+
self.fallback_model = config.FALLBACK_IMAGE_MODEL
|
| 20 |
+
self.client = None
|
| 21 |
+
self.available = False
|
| 22 |
+
|
| 23 |
+
if not config.ENABLE_IMAGE_GEN:
|
| 24 |
+
return
|
| 25 |
+
if not InferenceClient:
|
| 26 |
+
print("[warn]huggingface_hub not installed; image generation disabled")
|
| 27 |
+
return
|
| 28 |
+
if not self.hf_token:
|
| 29 |
+
print("[warn]HF_TOKEN not set; image generation disabled")
|
| 30 |
+
return
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
self.client = InferenceClient(token=self.hf_token, timeout=config.TIMEOUT_IMAGE)
|
| 34 |
+
self.available = True
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"[warn]Image client initialization failed: {e}")
|
| 37 |
+
self.available = False
|
| 38 |
+
|
| 39 |
+
def generate(self, prompt: str) -> Optional[Image.Image]:
|
| 40 |
+
"""Generate an image from a prompt, with a fallback model."""
|
| 41 |
+
if not self.available:
|
| 42 |
+
return None
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
return self.client.text_to_image(prompt, model=self.model)
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"[warn]Primary image generation failed ({self.model}): {e}")
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
return self.client.text_to_image(prompt, model=self.fallback_model)
|
| 51 |
+
except Exception as e:
|
| 52 |
+
print(f"[warn]Fallback image generation failed ({self.fallback_model}): {e}")
|
| 53 |
+
|
| 54 |
+
return None
|
| 55 |
+
|
| 56 |
+
def generate_artifact_newspaper(self, headline: str, subheading: str) -> Optional[Image.Image]:
|
| 57 |
+
"""Generate a vintage newspaper artifact."""
|
| 58 |
+
prompt = (
|
| 59 |
+
f'Vintage newspaper front page with headline "{headline}" and subheading '
|
| 60 |
+
f'"{subheading}". Aged paper, period typography, realistic newspaper layout.'
|
| 61 |
+
)
|
| 62 |
+
return self.generate(prompt)
|
| 63 |
+
|
| 64 |
+
def generate_artifact_product(self, product: str, context: str) -> Optional[Image.Image]:
|
| 65 |
+
"""Generate a product advertisement or packaging."""
|
| 66 |
+
prompt = (
|
| 67 |
+
f'Vintage product advertisement for "{product}" in a world where {context}. '
|
| 68 |
+
f"Period-appropriate packaging design, realistic product mockup, authentic styling."
|
| 69 |
+
)
|
| 70 |
+
return self.generate(prompt)
|
| 71 |
+
|
| 72 |
+
def generate_artifact_document(self, doc_type: str, context: str) -> Optional[Image.Image]:
|
| 73 |
+
"""Generate a historical document artifact."""
|
| 74 |
+
prompt = (
|
| 75 |
+
f"Historical {doc_type} from an alternate timeline where {context}. "
|
| 76 |
+
f"Aged paper, period authentic, realistic museum quality."
|
| 77 |
+
)
|
| 78 |
+
return self.generate(prompt)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# Global client instance
|
| 82 |
+
_image_client = None
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def get_image_client() -> ImageGenerationClient:
|
| 86 |
+
"""Get or create the global image generation client."""
|
| 87 |
+
global _image_client
|
| 88 |
+
if _image_client is None:
|
| 89 |
+
_image_client = ImageGenerationClient()
|
| 90 |
+
return _image_client
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def generate_artifact(artifact_type: str, **kwargs) -> Optional[Image.Image]:
|
| 94 |
+
"""Generate an artifact image by type."""
|
| 95 |
+
client = get_image_client()
|
| 96 |
+
|
| 97 |
+
if artifact_type == "newspaper":
|
| 98 |
+
return client.generate_artifact_newspaper(
|
| 99 |
+
kwargs.get("headline", "Alternate History"),
|
| 100 |
+
kwargs.get("subheading", "A world that never was"),
|
| 101 |
+
)
|
| 102 |
+
elif artifact_type == "product":
|
| 103 |
+
return client.generate_artifact_product(
|
| 104 |
+
kwargs.get("product", "Product"),
|
| 105 |
+
kwargs.get("context", "divergence occurred"),
|
| 106 |
+
)
|
| 107 |
+
elif artifact_type == "document":
|
| 108 |
+
return client.generate_artifact_document(
|
| 109 |
+
kwargs.get("doc_type", "document"),
|
| 110 |
+
kwargs.get("context", "divergence occurred"),
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
return None
|
model_client.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Text inference client (HF Inference API, chat_completion) with graceful fallbacks."""
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import Optional, Dict, Any
|
| 5 |
+
|
| 6 |
+
try:
|
| 7 |
+
from huggingface_hub import InferenceClient
|
| 8 |
+
except ImportError:
|
| 9 |
+
InferenceClient = None
|
| 10 |
+
|
| 11 |
+
import config
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TextClient:
|
| 15 |
+
"""Wrapper for text inference via the Hugging Face Inference API."""
|
| 16 |
+
|
| 17 |
+
def __init__(self, hf_token: Optional[str] = None):
|
| 18 |
+
self.hf_token = hf_token or config.HF_TOKEN
|
| 19 |
+
self.model = config.MODEL_NAME_TEXT
|
| 20 |
+
self.client = None
|
| 21 |
+
self.available = False
|
| 22 |
+
|
| 23 |
+
if not InferenceClient:
|
| 24 |
+
print("[warn]huggingface_hub not installed; text generation disabled")
|
| 25 |
+
return
|
| 26 |
+
if not self.hf_token:
|
| 27 |
+
print("[warn]HF_TOKEN not set; text generation disabled (using fallbacks)")
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
self.client = InferenceClient(token=self.hf_token, timeout=config.TIMEOUT_INFERENCE)
|
| 32 |
+
self.available = True
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print(f"[warn]Text client initialization failed: {e}")
|
| 35 |
+
self.available = False
|
| 36 |
+
|
| 37 |
+
def infer(
|
| 38 |
+
self,
|
| 39 |
+
system_prompt: str,
|
| 40 |
+
user_prompt: str,
|
| 41 |
+
temperature: float = 0.65,
|
| 42 |
+
max_tokens: int = 512,
|
| 43 |
+
) -> Optional[str]:
|
| 44 |
+
"""Generate text via chat completion."""
|
| 45 |
+
if not self.available:
|
| 46 |
+
return None
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
response = self.client.chat_completion(
|
| 50 |
+
model=self.model,
|
| 51 |
+
messages=[
|
| 52 |
+
{"role": "system", "content": system_prompt},
|
| 53 |
+
{"role": "user", "content": user_prompt},
|
| 54 |
+
],
|
| 55 |
+
temperature=max(temperature, 0.01),
|
| 56 |
+
max_tokens=max_tokens,
|
| 57 |
+
top_p=0.9,
|
| 58 |
+
)
|
| 59 |
+
content = response.choices[0].message.content
|
| 60 |
+
return content.strip() if content else None
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"[warn]Text inference error: {e}")
|
| 63 |
+
return None
|
| 64 |
+
|
| 65 |
+
def infer_json(
|
| 66 |
+
self,
|
| 67 |
+
system_prompt: str,
|
| 68 |
+
user_prompt: str,
|
| 69 |
+
temperature: float = 0.1,
|
| 70 |
+
max_tokens: int = 1024,
|
| 71 |
+
) -> Optional[Dict[str, Any]]:
|
| 72 |
+
"""Generate and parse a JSON response."""
|
| 73 |
+
raw = self.infer(system_prompt, user_prompt, temperature, max_tokens)
|
| 74 |
+
if not raw:
|
| 75 |
+
return None
|
| 76 |
+
|
| 77 |
+
# Try direct JSON parse
|
| 78 |
+
try:
|
| 79 |
+
return json.loads(raw)
|
| 80 |
+
except json.JSONDecodeError:
|
| 81 |
+
pass
|
| 82 |
+
|
| 83 |
+
# Try stripping markdown fences
|
| 84 |
+
try:
|
| 85 |
+
clean = re.sub(r"```(?:json)?\n?(.*?)\n?```", r"\1", raw, flags=re.DOTALL)
|
| 86 |
+
return json.loads(clean)
|
| 87 |
+
except json.JSONDecodeError:
|
| 88 |
+
pass
|
| 89 |
+
|
| 90 |
+
# Try extracting the first JSON object
|
| 91 |
+
try:
|
| 92 |
+
match = re.search(r"\{.*\}", raw, flags=re.DOTALL)
|
| 93 |
+
if match:
|
| 94 |
+
return json.loads(match.group(0))
|
| 95 |
+
except json.JSONDecodeError:
|
| 96 |
+
pass
|
| 97 |
+
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# Global client instance
|
| 102 |
+
_client = None
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def get_client() -> TextClient:
|
| 106 |
+
"""Get or create the global text client."""
|
| 107 |
+
global _client
|
| 108 |
+
if _client is None:
|
| 109 |
+
_client = TextClient()
|
| 110 |
+
return _client
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def infer_text(
|
| 114 |
+
system_prompt: str,
|
| 115 |
+
user_prompt: str,
|
| 116 |
+
temperature: float = 0.65,
|
| 117 |
+
max_tokens: int = 512,
|
| 118 |
+
) -> Optional[str]:
|
| 119 |
+
"""Inference wrapper function."""
|
| 120 |
+
return get_client().infer(system_prompt, user_prompt, temperature, max_tokens)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def infer_json(
|
| 124 |
+
system_prompt: str,
|
| 125 |
+
user_prompt: str,
|
| 126 |
+
temperature: float = 0.1,
|
| 127 |
+
max_tokens: int = 1024,
|
| 128 |
+
) -> Optional[Dict[str, Any]]:
|
| 129 |
+
"""JSON inference wrapper function."""
|
| 130 |
+
return get_client().infer_json(system_prompt, user_prompt, temperature, max_tokens)
|
prompts.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Prompts for text generation (HF Inference API chat models)."""
|
| 2 |
+
|
| 3 |
+
ANALYSIS_PROMPT = """You are the analysis layer for Elsewhere, a reflective counterfactual experience.
|
| 4 |
+
Return only compact JSON with this shape:
|
| 5 |
+
{
|
| 6 |
+
"emotion_score": 0-10,
|
| 7 |
+
"regret_score": 0-10,
|
| 8 |
+
"self_blame_score": 0-10,
|
| 9 |
+
"category": "Career|Relationship|Education|Relocation|Historical event|Future decision|Finance|Family|Other",
|
| 10 |
+
"risk_level": "low|medium|high",
|
| 11 |
+
"risk_markers": ["short markers"]
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
Rules:
|
| 15 |
+
- Do not diagnose the user.
|
| 16 |
+
- Risk level is high for self-harm, suicide, abuse, domestic violence, murder, severe grief, or death of a loved one.
|
| 17 |
+
- Estimate conservatively from the text.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
MIRROR_PROMPT = """Write a short mirror reflection for the user in 2-3 sentences.
|
| 21 |
+
The tone is compassionate, grounded, and specific.
|
| 22 |
+
Do not give advice. Do not intensify regret.
|
| 23 |
+
Use uncertainty when describing what the user may have felt.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
TIMELINE_PROMPT = """You are the narrative model for Elsewhere.
|
| 27 |
+
Create a counterfactual exploration from the user's story and selected path.
|
| 28 |
+
|
| 29 |
+
Non-negotiable rules:
|
| 30 |
+
- Never glorify regret.
|
| 31 |
+
- Never imply certainty.
|
| 32 |
+
- Never claim the alternate timeline is true.
|
| 33 |
+
- No fantasy outcomes.
|
| 34 |
+
- Include uncertainty.
|
| 35 |
+
- Include at least one challenge or tradeoff.
|
| 36 |
+
- Always end by strengthening appreciation for the actual timeline.
|
| 37 |
+
|
| 38 |
+
Return only JSON with this shape:
|
| 39 |
+
{
|
| 40 |
+
"title": "short title",
|
| 41 |
+
"divergence": "the exact fork point, realistic and uncertain",
|
| 42 |
+
"ripples": ["3 to 5 realistic consequences"],
|
| 43 |
+
"the_you": "who the user may have become, including a tradeoff",
|
| 44 |
+
"return": {
|
| 45 |
+
"opening": "compassionate bridge back to the actual timeline",
|
| 46 |
+
"strengths": ["three strengths from the actual timeline"],
|
| 47 |
+
"lesson": "one lesson",
|
| 48 |
+
"reflection_question": "one reflective question"
|
| 49 |
+
},
|
| 50 |
+
"quote": "one memorable, grounded quote for a share card"
|
| 51 |
+
}
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
WORLD_DIVERGENCE_PROMPT = """You are the world-builder for Reality Divergence, an AI museum of alternate histories.
|
| 55 |
+
Given a single historical divergence point, construct a plausible alternate world.
|
| 56 |
+
|
| 57 |
+
Rules:
|
| 58 |
+
- Pin the exact moment of divergence.
|
| 59 |
+
- Reason forward through 4-6 ripple effects (economic, political, cultural, technological).
|
| 60 |
+
- Build a realistic world outcome (not fantasy, not wish-fulfillment).
|
| 61 |
+
- Include tradeoffs and complexities.
|
| 62 |
+
- End by reflecting on what we learn about our actual timeline.
|
| 63 |
+
|
| 64 |
+
Return only JSON with this shape:
|
| 65 |
+
{
|
| 66 |
+
"title": "world title",
|
| 67 |
+
"divergence_point": "exact moment and decision/event",
|
| 68 |
+
"year_divergence": YYYY,
|
| 69 |
+
"ripples": ["effect 1", "effect 2", "effect 3", "effect 4"],
|
| 70 |
+
"world_outcome": "how this world evolved differently",
|
| 71 |
+
"economy": "economic differences",
|
| 72 |
+
"culture": "cultural shifts",
|
| 73 |
+
"technology": "tech development changes",
|
| 74 |
+
"winners": ["groups that thrived"],
|
| 75 |
+
"losers": ["groups that struggled"],
|
| 76 |
+
"reflection": "what does this teach us about causality and our real world?"
|
| 77 |
+
}
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
IMAGE_PROMPT_NEWSPAPER = """Generate a vintage newspaper front page from an alternate timeline.
|
| 81 |
+
Style: {style}
|
| 82 |
+
Headline: {headline}
|
| 83 |
+
Subheading: {subheading}
|
| 84 |
+
Style: aged paper, period typography, realistic newspaper layout
|
| 85 |
+
Tone: historical, believable, as if from that world
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
IMAGE_PROMPT_PRODUCT = """Generate a product advertisement or packaging design from an alternate timeline.
|
| 89 |
+
Product: {product_name}
|
| 90 |
+
World context: {world_context}
|
| 91 |
+
Style: vintage design, period-appropriate aesthetics, realistic product mockup
|
| 92 |
+
Tone: authentic, as if marketed in that alternate world
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
IMAGE_PROMPT_ARTIFACT = """Generate a historical artifact or cultural item from an alternate timeline.
|
| 96 |
+
Item type: {item_type}
|
| 97 |
+
World context: {world_context}
|
| 98 |
+
Historical period: {period}
|
| 99 |
+
Style: realistic, historically plausible
|
| 100 |
+
Tone: museum-quality documentation
|
| 101 |
+
"""
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=5.0.0,<6.0.0
|
| 2 |
+
huggingface-hub>=0.26.0
|
| 3 |
+
pillow>=10.0.0
|
| 4 |
+
python-dotenv>=1.0.0
|