prashantAI Claude Opus 4.8 (1M context) commited on
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>

Files changed (10) hide show
  1. .env.example +7 -0
  2. .gitignore +56 -0
  3. README.md +56 -3
  4. STATUS.md +60 -0
  5. app.py +626 -25
  6. config.py +63 -0
  7. image_client.py +113 -0
  8. model_client.py +130 -0
  9. prompts.py +101 -0
  10. 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: 6.17.3
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
 
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- def generate_reality(event):
4
- return f"""
5
- Reality Divergence
 
 
 
 
 
 
6
 
7
- Scenario:
8
- {event}
 
 
9
 
10
- Alternate Timeline:
 
 
 
11
 
12
- 2026
13
- A major divergence occurs.
 
 
14
 
15
- 2028
16
- Technology evolves differently.
 
 
17
 
18
- 2035
19
- The world has changed dramatically.
 
 
 
 
 
20
 
21
- 2050
22
- A completely new reality emerges.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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