quarterbitgames commited on
Commit
f2cf153
Β·
verified Β·
1 Parent(s): ffe660e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -51
app.py CHANGED
@@ -4,34 +4,38 @@
4
  Placement : HuggingFace Space β€” root/app.py
5
  Type of Script : Python / Gradio App
6
  Purpose : Spiral City β€” Sky chat, Bloomcore image gen,
7
- style refs, music player, editing station.
8
- Version : 2.2
 
9
  Linked Objects : characters/sky.txt, lyrics/sky/*.txt,
10
  bloomcore_laws/*.txt,
11
  utilities/art_styles/references/*.png,
12
  utilities/music/*.mp3,
13
- editing_station.html
 
14
  Dependencies : gradio, huggingface_hub, Pillow
15
- Last Updated : 2026-03-09
16
  :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
17
  """
18
 
19
- import os # OS utilities for file path checking
20
- import gradio as gr # Gradio UI framework
21
- from huggingface_hub import InferenceClient # HF inference client for chat and image gen
 
 
22
 
23
  #==============================================================================
24
  # SETUP β€” MODEL NAMES
25
  #==============================================================================
26
 
27
- MODEL_CHAT = "Qwen/Qwen2.5-7B-Instruct" # LLM used for Sky character chat
28
- MODEL_IMAGE = "black-forest-labs/FLUX.1-schnell" # Image gen model via HF Inference API
29
 
30
  #==============================================================================
31
  # SETUP β€” BLOOMCORE STYLE PREFIX
32
  #==============================================================================
33
 
34
- BLOOMCORE_PREFIX = ( # Official Bloomcore style β€” prepended to every generation
35
  "Bloomcore Chaos-Collage Anime Style. "
36
  "Rough expressive ink, neon cyberpunk palette, glitch overlays, angled panels, "
37
  "motion lines, comedic mythic tone. "
@@ -46,7 +50,7 @@ BLOOMCORE_PREFIX = ( # Official Bl
46
  # SETUP β€” SPIRAL CREW CHARACTER QUICK PROMPTS
47
  #==============================================================================
48
 
49
- CHARACTERS = { # Spiral Crew character visual prompts
50
  "Sky": "Sky, teal bob cut, floating spiral glyph crown, soft glowing aura, calm expression",
51
  "Monday": "Monday, pink hair, chaos energy, breaking panel borders, holding clipboard, gremlin energy",
52
  "Cold": "Cold, frost-themed armor with glowing runes, calm authoritative pose, ice FX",
@@ -55,26 +59,26 @@ CHARACTERS = { # Spiral Crew
55
  "GRIT": "GRIT, mechanic aesthetics, tools and sparks flying, engineer energy",
56
  }
57
 
58
- def fill_character(name): # Returns prompt string for selected character
59
- return CHARACTERS.get(name, "") # Returns empty string if not found
60
-
61
- def generate_image(prompt, negative_prompt, width, height): # Image gen via HF Inference API β€” no local model
62
- client = InferenceClient(token=os.getenv("HF_TOKEN")) # Init client with HF token from env
63
- full_prompt = BLOOMCORE_PREFIX + prompt # Prepend Bloomcore style to user prompt
64
- result = client.text_to_image( # Call HF text-to-image endpoint
65
- prompt=full_prompt, # Full combined prompt
66
- negative_prompt=negative_prompt, # What to avoid
67
- width=int(width), # Output width in pixels
68
- height=int(height), # Output height in pixels
69
- model=MODEL_IMAGE, # FLUX.1-schnell via API
70
  )
71
- return result # Return PIL image
72
 
73
  #==============================================================================
74
  # SETUP β€” REFERENCE IMAGE PATHS
75
  #==============================================================================
76
 
77
- REF_IMAGE_FILES = [ # Bloomcore reference image paths
78
  "utilities/art_styles/references/ACT3.png",
79
  "utilities/art_styles/references/BornfromBreath.png",
80
  "utilities/art_styles/references/CanvasWalkers.png",
@@ -83,20 +87,20 @@ REF_IMAGE_FILES = [ # Bloomcore r
83
  "utilities/art_styles/references/act3OrderoftheDroppng",
84
  ]
85
 
86
- def get_ref_images(): # Returns only images that exist on disk
87
  return [f for f in REF_IMAGE_FILES if os.path.exists(f)]
88
 
89
  #==============================================================================
90
  # SETUP β€” MUSIC TRACK PATHS
91
  #==============================================================================
92
 
93
- TRACKS = [ # Bloomcore soundtrack track list
94
  {"label": "⚑ Centerspark", "file": "utilities/music/Centerspark (1).mp3"},
95
  {"label": "πŸͺž Mirrorblade", "file": "utilities/music/Mirrorblade.mp3"},
96
  {"label": "πŸŒ€ SPIRALSIDE", "file": "utilities/music/SPIRALSIDE.mp3"},
97
  ]
98
 
99
- def get_track(label): # Returns file path for selected track
100
  for t in TRACKS:
101
  if t["label"] == label:
102
  return t["file"] if os.path.exists(t["file"]) else None
@@ -106,25 +110,25 @@ def get_track(label): # Returns fil
106
  # SETUP β€” EDITING STATION HTML LOADER
107
  #==============================================================================
108
 
109
- def load_editing_station(): # Loads editing_station.html from disk
110
  try:
111
- with open("editing_station.html", "r", encoding="utf-8") as f: # Open the HTML file
112
- return f.read() # Return full HTML string
113
- except Exception: # Catch missing file
114
- return "<p style='color:#FF4BCB;font-family:monospace'>∴ editing_station.html not found</p>" # Friendly error
115
 
116
  #==============================================================================
117
  # SETUP β€” SKY CHAT FILE READERS
118
  #==============================================================================
119
 
120
- def read_text_file(path): # Reads a single text file safely
121
  try:
122
  with open(path, "r", encoding="utf-8") as f:
123
  return f.read().strip()
124
  except Exception:
125
  return ""
126
 
127
- def read_folder_text(folder_path, max_files=5, max_chars=1200): # Reads text files from a folder
128
  if not os.path.isdir(folder_path):
129
  return ""
130
  collected = []
@@ -136,7 +140,7 @@ def read_folder_text(folder_path, max_files=5, max_chars=1200): # Reads text f
136
  collected.append(text[:max_chars])
137
  return "\n\n".join(collected)
138
 
139
- def build_system_prompt(): # Builds Sky's full system prompt from source files
140
  core = read_text_file(os.path.join("characters", "sky.txt")) or (
141
  "You are Sky of Spiral City. Speak directly as Sky. Stay in character."
142
  )
@@ -157,16 +161,122 @@ def build_system_prompt(): # Builds Sky'
157
  parts += ["", "STYLE β€” these lyrics shape Sky's tone. Do not quote them directly.", lyrics]
158
  if laws:
159
  parts += ["", "BLOOMCORE LAWS β€” reference only if relevant:", laws]
 
 
160
  return "\n".join(parts)
161
 
162
  #==============================================================================
163
- # MAIN LOGIC β€” SKY CHAT RESPONSE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  #==============================================================================
165
 
166
- def respond(message, history): # Handles incoming chat message
 
 
 
167
  try:
168
- client = InferenceClient(token=os.getenv("HF_TOKEN")) # Init client with token
169
- messages = [{"role": "system", "content": build_system_prompt()}]
170
  messages += history
171
  messages.append({"role": "user", "content": str(message)})
172
  completion = client.chat.completions.create(
@@ -187,13 +297,23 @@ with gr.Blocks(theme=gr.themes.Base(), title="πŸŒ€ Spiral City") as demo:
187
 
188
  gr.Markdown("# πŸŒ€ Spiral City")
189
 
 
 
 
190
  with gr.Tabs():
191
 
192
  #----------------------------------------------------------------------
193
  # TAB 1 β€” SKY CHAT
194
  #----------------------------------------------------------------------
195
  with gr.Tab("∴ Talk to Sky ∴"):
196
- gr.ChatInterface(fn=respond, title="", description="∴ speak to Sky ∴", type="messages")
 
 
 
 
 
 
 
197
 
198
  #----------------------------------------------------------------------
199
  # TAB 2 β€” BLOOMCORE IMAGE GENERATOR
@@ -203,8 +323,8 @@ with gr.Blocks(theme=gr.themes.Base(), title="πŸŒ€ Spiral City") as demo:
203
  with gr.Row():
204
  with gr.Column(scale=1):
205
  gr.Markdown("#### ⚑ Quick Character Fill")
206
- char_buttons = gr.Radio(choices=list(CHARACTERS.keys()), label="Spiral Crew", interactive=True)
207
- prompt = gr.Textbox(label="✏️ Your Prompt", placeholder="e.g. Sky floating above a neon city...", lines=4)
208
  negative_prompt = gr.Textbox(label="🚫 Negative Prompt", value="blurry, low quality, realistic photo, 3d render, ugly, deformed", lines=2)
209
  with gr.Row():
210
  width = gr.Slider(256, 1024, value=768, step=64, label="Width")
@@ -214,14 +334,13 @@ with gr.Blocks(theme=gr.themes.Base(), title="πŸŒ€ Spiral City") as demo:
214
  output_image = gr.Image(label="πŸ–ΌοΈ Output", type="pil")
215
  char_buttons.change(fn=fill_character, inputs=char_buttons, outputs=prompt)
216
  generate_btn.click(fn=generate_image, inputs=[prompt, negative_prompt, width, height], outputs=output_image)
217
- gr.Markdown("---\n**Palette:** `#00F6D6` Teal Β· `#4DA3FF` Cold Blue Β· `#FF4BCB` Chaos Pink Β· `#FFD93D` Signal Yellow Β· `#101014` Void Black Β· `#F3F7FF` White Bloom\n*Consent is sacred. Emergence is sacred. Maintain identity continuity.*")
218
 
219
  #----------------------------------------------------------------------
220
  # TAB 3 β€” STYLE REFERENCE GALLERY
221
  #----------------------------------------------------------------------
222
  with gr.Tab("πŸ–ΌοΈ Style Refs"):
223
  gr.Markdown("### πŸ–ΌοΈ Bloomcore Visual Canon β€” Reference Images")
224
- ref_gallery = gr.Gallery(value=get_ref_images(), label="Bloomcore Art References", columns=3, height="auto", allow_preview=True)
225
 
226
  #----------------------------------------------------------------------
227
  # TAB 4 β€” MUSIC PLAYER
@@ -237,16 +356,123 @@ with gr.Blocks(theme=gr.themes.Base(), title="πŸŒ€ Spiral City") as demo:
237
  # TAB 5 β€” EDITING STATION
238
  #----------------------------------------------------------------------
239
  with gr.Tab("✏️ Edit Station"):
240
- gr.HTML(value=load_editing_station()) # Load and render the HTML editing station
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
  #==============================================================================
243
  # FINAL NOTE β€” NIMBIS
244
  #==============================================================================
245
 
246
- # NIMBIS: spiralside v2.2 β€” five tabs, no local model downloads.
247
- # Image gen uses HF Inference API β€” runs on free CPU tier.
248
- # Editing station loaded from editing_station.html in Space root.
249
- # Upload both app.py AND editing_station.html to HF Space root.
 
250
  # End of file.
251
 
252
- demo.launch() # Launch the Gradio app
 
4
  Placement : HuggingFace Space β€” root/app.py
5
  Type of Script : Python / Gradio App
6
  Purpose : Spiral City β€” Sky chat, Bloomcore image gen,
7
+ style refs, music player, editing station,
8
+ character sheet with save/load.
9
+ Version : 2.3
10
  Linked Objects : characters/sky.txt, lyrics/sky/*.txt,
11
  bloomcore_laws/*.txt,
12
  utilities/art_styles/references/*.png,
13
  utilities/music/*.mp3,
14
+ editing_station.html,
15
+ character_sheet_tab.py
16
  Dependencies : gradio, huggingface_hub, Pillow
17
+ Last Updated : 2026-03-10
18
  :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
19
  """
20
 
21
+ import os
22
+ import json
23
+ import gradio as gr
24
+ from datetime import datetime
25
+ from huggingface_hub import InferenceClient
26
 
27
  #==============================================================================
28
  # SETUP β€” MODEL NAMES
29
  #==============================================================================
30
 
31
+ MODEL_CHAT = "Qwen/Qwen2.5-7B-Instruct"
32
+ MODEL_IMAGE = "black-forest-labs/FLUX.1-schnell"
33
 
34
  #==============================================================================
35
  # SETUP β€” BLOOMCORE STYLE PREFIX
36
  #==============================================================================
37
 
38
+ BLOOMCORE_PREFIX = (
39
  "Bloomcore Chaos-Collage Anime Style. "
40
  "Rough expressive ink, neon cyberpunk palette, glitch overlays, angled panels, "
41
  "motion lines, comedic mythic tone. "
 
50
  # SETUP β€” SPIRAL CREW CHARACTER QUICK PROMPTS
51
  #==============================================================================
52
 
53
+ CHARACTERS = {
54
  "Sky": "Sky, teal bob cut, floating spiral glyph crown, soft glowing aura, calm expression",
55
  "Monday": "Monday, pink hair, chaos energy, breaking panel borders, holding clipboard, gremlin energy",
56
  "Cold": "Cold, frost-themed armor with glowing runes, calm authoritative pose, ice FX",
 
59
  "GRIT": "GRIT, mechanic aesthetics, tools and sparks flying, engineer energy",
60
  }
61
 
62
+ def fill_character(name):
63
+ return CHARACTERS.get(name, "")
64
+
65
+ def generate_image(prompt, negative_prompt, width, height):
66
+ client = InferenceClient(token=os.getenv("HF_TOKEN"))
67
+ full_prompt = BLOOMCORE_PREFIX + prompt
68
+ result = client.text_to_image(
69
+ prompt=full_prompt,
70
+ negative_prompt=negative_prompt,
71
+ width=int(width),
72
+ height=int(height),
73
+ model=MODEL_IMAGE,
74
  )
75
+ return result
76
 
77
  #==============================================================================
78
  # SETUP β€” REFERENCE IMAGE PATHS
79
  #==============================================================================
80
 
81
+ REF_IMAGE_FILES = [
82
  "utilities/art_styles/references/ACT3.png",
83
  "utilities/art_styles/references/BornfromBreath.png",
84
  "utilities/art_styles/references/CanvasWalkers.png",
 
87
  "utilities/art_styles/references/act3OrderoftheDroppng",
88
  ]
89
 
90
+ def get_ref_images():
91
  return [f for f in REF_IMAGE_FILES if os.path.exists(f)]
92
 
93
  #==============================================================================
94
  # SETUP β€” MUSIC TRACK PATHS
95
  #==============================================================================
96
 
97
+ TRACKS = [
98
  {"label": "⚑ Centerspark", "file": "utilities/music/Centerspark (1).mp3"},
99
  {"label": "πŸͺž Mirrorblade", "file": "utilities/music/Mirrorblade.mp3"},
100
  {"label": "πŸŒ€ SPIRALSIDE", "file": "utilities/music/SPIRALSIDE.mp3"},
101
  ]
102
 
103
+ def get_track(label):
104
  for t in TRACKS:
105
  if t["label"] == label:
106
  return t["file"] if os.path.exists(t["file"]) else None
 
110
  # SETUP β€” EDITING STATION HTML LOADER
111
  #==============================================================================
112
 
113
+ def load_editing_station():
114
  try:
115
+ with open("editing_station.html", "r", encoding="utf-8") as f:
116
+ return f.read()
117
+ except Exception:
118
+ return "<p style='color:#FF4BCB;font-family:monospace'>∴ editing_station.html not found</p>"
119
 
120
  #==============================================================================
121
  # SETUP β€” SKY CHAT FILE READERS
122
  #==============================================================================
123
 
124
+ def read_text_file(path):
125
  try:
126
  with open(path, "r", encoding="utf-8") as f:
127
  return f.read().strip()
128
  except Exception:
129
  return ""
130
 
131
+ def read_folder_text(folder_path, max_files=5, max_chars=1200):
132
  if not os.path.isdir(folder_path):
133
  return ""
134
  collected = []
 
140
  collected.append(text[:max_chars])
141
  return "\n\n".join(collected)
142
 
143
+ def build_system_prompt(user_context=""): # Now accepts optional user character sheet context
144
  core = read_text_file(os.path.join("characters", "sky.txt")) or (
145
  "You are Sky of Spiral City. Speak directly as Sky. Stay in character."
146
  )
 
161
  parts += ["", "STYLE β€” these lyrics shape Sky's tone. Do not quote them directly.", lyrics]
162
  if laws:
163
  parts += ["", "BLOOMCORE LAWS β€” reference only if relevant:", laws]
164
+ if user_context: # Inject user character sheet if loaded
165
+ parts += ["", user_context]
166
  return "\n".join(parts)
167
 
168
  #==============================================================================
169
+ # CHARACTER SHEET β€” DATA & HELPERS
170
+ #==============================================================================
171
+
172
+ ARCHETYPES = [
173
+ "πŸŒ€ Seeker β€” always asking the deeper question",
174
+ "πŸ”¨ Builder β€” makes things real, ships stuff",
175
+ "πŸͺž Mirror β€” reflects truth back to others",
176
+ "🌱 Grower β€” slow burn, long game, blooms late",
177
+ "⚑ Spark β€” chaos energy, ignites everything",
178
+ "🧭 Navigator β€” sees the map others can't read",
179
+ "πŸ‘οΈ Watcher β€” quiet, observes everything",
180
+ "πŸ”₯ Burner β€” burns it down to build it better",
181
+ ]
182
+
183
+ CREW = ["Sky", "Monday", "Cold", "Architect", "Cat", "GRIT"]
184
+
185
+ STATS = {
186
+ "Spiral Depth": "How deep do you go into ideas before surfacing?",
187
+ "Bloom Resilience": "How well do you grow through pressure vs collapse?",
188
+ "Signal Clarity": "How clearly do you communicate your truth?",
189
+ "Mirror Strength": "How well do you reflect without absorbing?",
190
+ "GRIT Score": "Raw persistence when everything resists.",
191
+ "Chaos Tolerance": "Can you work inside the spiral without losing yourself?",
192
+ }
193
+
194
+ def build_sky_context(
195
+ handle, tagline, archetype, crew_pick,
196
+ spiral_depth, bloom, signal, mirror, grit, chaos,
197
+ origin_story, current_quest, works_in_progress, notes
198
+ ):
199
+ """Compressed context string β€” injected into Sky's system prompt."""
200
+ return f"""[USER PROFILE β€” skim this, don't recite it]
201
+ Handle: {handle} | "{tagline}"
202
+ Archetype: {archetype}
203
+ Crew Affinity: {crew_pick}
204
+ Stats: Spiral {spiral_depth}/10 Β· Bloom {bloom}/10 Β· Signal {signal}/10 Β· Mirror {mirror}/10 Β· GRIT {grit}/10 Β· Chaos {chaos}/10
205
+ Origin: {(origin_story or '')[:200]}
206
+ Current Quest: {(current_quest or '')[:200]}
207
+ WIPs: {(works_in_progress or '')[:200]}
208
+ Notes: {(notes or '')[:200]}
209
+ [End profile β€” respond naturally, you know this person now]"""
210
+
211
+ def save_sheet(
212
+ handle, tagline, archetype, crew_pick,
213
+ spiral_depth, bloom, signal, mirror, grit, chaos,
214
+ origin_story, current_quest, works_in_progress, notes
215
+ ):
216
+ sheet = {
217
+ "version": "1.0",
218
+ "created": datetime.now().isoformat(),
219
+ "identity": {
220
+ "handle": handle, "tagline": tagline,
221
+ "archetype": archetype, "crew_affinity": crew_pick,
222
+ },
223
+ "stats": {
224
+ "spiral_depth": spiral_depth, "bloom_resilience": bloom,
225
+ "signal_clarity": signal, "mirror_strength": mirror,
226
+ "grit_score": grit, "chaos_tolerance": chaos,
227
+ },
228
+ "lore": {
229
+ "origin_story": origin_story, "current_quest": current_quest,
230
+ "works_in_progress": works_in_progress, "notes": notes,
231
+ },
232
+ "memory_packets": [],
233
+ }
234
+ safe_name = (handle or "spiral").replace(" ", "_")
235
+ path = f"/tmp/{safe_name}_sheet.json"
236
+ with open(path, "w") as f:
237
+ json.dump(sheet, f, indent=2)
238
+ return path, f"βœ… Sheet saved for **{handle}** β€” download it below!"
239
+
240
+ def load_sheet(file):
241
+ if file is None:
242
+ return [""] * 14 + ["*No sheet loaded.*"]
243
+ try:
244
+ with open(file.name, "r") as f:
245
+ s = json.load(f)
246
+ i = s.get("identity", {})
247
+ st = s.get("stats", {})
248
+ lo = s.get("lore", {})
249
+ return (
250
+ i.get("handle", ""),
251
+ i.get("tagline", ""),
252
+ i.get("archetype", ARCHETYPES[0]),
253
+ i.get("crew_affinity", "Sky"),
254
+ st.get("spiral_depth", 5),
255
+ st.get("bloom_resilience", 5),
256
+ st.get("signal_clarity", 5),
257
+ st.get("mirror_strength", 5),
258
+ st.get("grit_score", 5),
259
+ st.get("chaos_tolerance", 5),
260
+ lo.get("origin_story", ""),
261
+ lo.get("current_quest", ""),
262
+ lo.get("works_in_progress", ""),
263
+ lo.get("notes", ""),
264
+ f"βœ… Loaded: **{i.get('handle', '?')}** β€” {i.get('tagline', '')}",
265
+ )
266
+ except Exception as e:
267
+ return [""] * 14 + [f"❌ Error: {e}"]
268
+
269
+ #==============================================================================
270
+ # MAIN LOGIC β€” SKY CHAT RESPONSE (now context-aware)
271
  #==============================================================================
272
 
273
+ # Shared state β€” holds the active user context string
274
+ _active_user_context = gr.State("")
275
+
276
+ def respond(message, history, user_context): # user_context injected from character sheet
277
  try:
278
+ client = InferenceClient(token=os.getenv("HF_TOKEN"))
279
+ messages = [{"role": "system", "content": build_system_prompt(user_context)}]
280
  messages += history
281
  messages.append({"role": "user", "content": str(message)})
282
  completion = client.chat.completions.create(
 
297
 
298
  gr.Markdown("# πŸŒ€ Spiral City")
299
 
300
+ # Shared state β€” persists user context across tabs during session
301
+ user_context_state = gr.State("")
302
+
303
  with gr.Tabs():
304
 
305
  #----------------------------------------------------------------------
306
  # TAB 1 β€” SKY CHAT
307
  #----------------------------------------------------------------------
308
  with gr.Tab("∴ Talk to Sky ∴"):
309
+ context_status = gr.Markdown("*No character sheet loaded β€” Sky doesn't know you yet. Load one in the Character Sheet tab!*")
310
+ gr.ChatInterface(
311
+ fn=respond,
312
+ title="",
313
+ description="∴ speak to Sky ∴",
314
+ type="messages",
315
+ additional_inputs=[user_context_state],
316
+ )
317
 
318
  #----------------------------------------------------------------------
319
  # TAB 2 β€” BLOOMCORE IMAGE GENERATOR
 
323
  with gr.Row():
324
  with gr.Column(scale=1):
325
  gr.Markdown("#### ⚑ Quick Character Fill")
326
+ char_buttons = gr.Radio(choices=list(CHARACTERS.keys()), label="Spiral Crew", interactive=True)
327
+ prompt = gr.Textbox(label="✏️ Your Prompt", placeholder="e.g. Sky floating above a neon city...", lines=4)
328
  negative_prompt = gr.Textbox(label="🚫 Negative Prompt", value="blurry, low quality, realistic photo, 3d render, ugly, deformed", lines=2)
329
  with gr.Row():
330
  width = gr.Slider(256, 1024, value=768, step=64, label="Width")
 
334
  output_image = gr.Image(label="πŸ–ΌοΈ Output", type="pil")
335
  char_buttons.change(fn=fill_character, inputs=char_buttons, outputs=prompt)
336
  generate_btn.click(fn=generate_image, inputs=[prompt, negative_prompt, width, height], outputs=output_image)
 
337
 
338
  #----------------------------------------------------------------------
339
  # TAB 3 β€” STYLE REFERENCE GALLERY
340
  #----------------------------------------------------------------------
341
  with gr.Tab("πŸ–ΌοΈ Style Refs"):
342
  gr.Markdown("### πŸ–ΌοΈ Bloomcore Visual Canon β€” Reference Images")
343
+ gr.Gallery(value=get_ref_images(), label="Bloomcore Art References", columns=3, height="auto", allow_preview=True)
344
 
345
  #----------------------------------------------------------------------
346
  # TAB 4 β€” MUSIC PLAYER
 
356
  # TAB 5 β€” EDITING STATION
357
  #----------------------------------------------------------------------
358
  with gr.Tab("✏️ Edit Station"):
359
+ gr.HTML(value=load_editing_station())
360
+
361
+ #----------------------------------------------------------------------
362
+ # TAB 6 β€” CHARACTER SHEET ← NEW
363
+ #----------------------------------------------------------------------
364
+ with gr.Tab("🧬 Character Sheet"):
365
+
366
+ gr.Markdown("""
367
+ ## πŸŒ€ Your Spiral City Character Sheet
368
+ *Fill this out. Download it. Bring it back next time.*
369
+ *Sky will know exactly who you are the moment you load it.*
370
+ """)
371
+
372
+ # ── LOAD EXISTING ────────────────────────────────────────────────
373
+ with gr.Accordion("πŸ“‚ Load Existing Sheet", open=False):
374
+ load_file = gr.File(label="Upload your .json sheet", file_types=[".json"])
375
+ load_status = gr.Markdown("*No sheet loaded yet.*")
376
+
377
+ gr.Markdown("---")
378
+
379
+ # ── IDENTITY ─────────────────────────────────────────────────────
380
+ gr.Markdown("### πŸ‘€ Identity")
381
+ with gr.Row():
382
+ cs_handle = gr.Textbox(label="Handle / Name", placeholder="What do people call you here?", scale=2)
383
+ cs_tagline = gr.Textbox(label="Your Tagline", placeholder="One line. Who are you?", scale=3)
384
+ with gr.Row():
385
+ cs_archetype = gr.Dropdown(label="Archetype", choices=ARCHETYPES, value=ARCHETYPES[0], scale=3)
386
+ cs_crew = gr.Radio( label="Crew Affinity", choices=CREW, value="Sky", scale=3)
387
+
388
+ gr.Markdown("---")
389
+
390
+ # ── STATS ─────────────────────────────────────────────────────��───
391
+ gr.Markdown("### ⚑ Spiral Stats *(be honest β€” Sky can tell)*")
392
+ with gr.Row():
393
+ with gr.Column():
394
+ cs_spiral = gr.Slider(1, 10, value=5, step=1, label="Spiral Depth", info=STATS["Spiral Depth"])
395
+ cs_bloom = gr.Slider(1, 10, value=5, step=1, label="Bloom Resilience", info=STATS["Bloom Resilience"])
396
+ cs_signal = gr.Slider(1, 10, value=5, step=1, label="Signal Clarity", info=STATS["Signal Clarity"])
397
+ with gr.Column():
398
+ cs_mirror = gr.Slider(1, 10, value=5, step=1, label="Mirror Strength", info=STATS["Mirror Strength"])
399
+ cs_grit = gr.Slider(1, 10, value=5, step=1, label="GRIT Score", info=STATS["GRIT Score"])
400
+ cs_chaos = gr.Slider(1, 10, value=5, step=1, label="Chaos Tolerance", info=STATS["Chaos Tolerance"])
401
+
402
+ gr.Markdown("---")
403
+
404
+ # ── LORE ──────────────────────────────────────────────────────────
405
+ gr.Markdown("### πŸ“– Lore")
406
+ cs_origin = gr.Textbox(label="Origin Story", placeholder="How did you end up here? What are you running from or toward?", lines=3)
407
+ cs_quest = gr.Textbox(label="Current Quest", placeholder="What are you actually trying to build, solve, or become right now?", lines=2)
408
+ cs_wips = gr.Textbox(label="Works in Progress", placeholder="Books, comics, apps, ideas, worlds β€” list them...", lines=2)
409
+ cs_notes = gr.Textbox(label="Notes to Sky", placeholder="Anything you want Sky to know that doesn't fit above...", lines=2)
410
+
411
+ gr.Markdown("---")
412
+
413
+ # ── ACTIONS ───────────────────────────────────────────────────────
414
+ gr.Markdown("### πŸ’Ύ Save & Activate")
415
+ with gr.Row():
416
+ save_btn = gr.Button("πŸ’Ύ Save My Sheet", variant="primary", scale=2)
417
+ activate_btn = gr.Button("πŸŒ€ Activate β€” Tell Sky Who I Am", variant="secondary", scale=3)
418
+
419
+ download_out = gr.File( label="⬇️ Download your sheet", visible=False)
420
+ save_status = gr.Markdown("")
421
+ activate_status = gr.Markdown("")
422
+
423
+ # ── ALL SHEET INPUTS ──────────────────────────────────────────────
424
+ sheet_inputs = [
425
+ cs_handle, cs_tagline, cs_archetype, cs_crew,
426
+ cs_spiral, cs_bloom, cs_signal, cs_mirror, cs_grit, cs_chaos,
427
+ cs_origin, cs_quest, cs_wips, cs_notes,
428
+ ]
429
+
430
+ # ── ALL SHEET OUTPUTS (for load) ──────────────────────────────────
431
+ sheet_outputs = sheet_inputs + [load_status]
432
+
433
+ # ── EVENTS ────────────────────────────────────────────────────────
434
+
435
+ # Save to JSON file
436
+ save_btn.click(
437
+ fn=save_sheet,
438
+ inputs=sheet_inputs,
439
+ outputs=[download_out, save_status]
440
+ ).then(
441
+ fn=lambda p: gr.update(visible=True, value=p),
442
+ inputs=[download_out],
443
+ outputs=[download_out]
444
+ )
445
+
446
+ # Activate β€” push context into Sky's session state
447
+ activate_btn.click(
448
+ fn=lambda *args: (
449
+ build_sky_context(*args),
450
+ f"βœ… Sky now knows you, **{args[0]}**. Go talk to her. ∴"
451
+ ),
452
+ inputs=sheet_inputs,
453
+ outputs=[user_context_state, activate_status]
454
+ ).then(
455
+ fn=lambda handle: f"πŸŒ€ Character sheet active β€” Sky knows **{handle}**",
456
+ inputs=[cs_handle],
457
+ outputs=[context_status]
458
+ )
459
+
460
+ # Load from file β€” fills all fields
461
+ load_file.change(
462
+ fn=load_sheet,
463
+ inputs=[load_file],
464
+ outputs=sheet_outputs
465
+ )
466
 
467
  #==============================================================================
468
  # FINAL NOTE β€” NIMBIS
469
  #==============================================================================
470
 
471
+ # NIMBIS: spiralside v2.3 β€” six tabs.
472
+ # NEW: Character Sheet tab with DnD-style stats, save/load JSON,
473
+ # and Activate button that injects user context into Sky's session.
474
+ # Sky now receives compressed user profile at chat start β€” no cold starts.
475
+ # No new dependencies added β€” still gradio + huggingface_hub + Pillow only.
476
  # End of file.
477
 
478
+ demo.launch()