ferrariedhgs commited on
Commit
81ee79c
Β·
verified Β·
1 Parent(s): e454688

Upload 3 files

Browse files
Files changed (2) hide show
  1. app.py +43 -26
  2. custom_styles.py +451 -451
app.py CHANGED
@@ -287,18 +287,20 @@ def on_continue(narrative_text, state, container_idx, key_id):
287
  Wrong/no-key β†’ pick a random static failure message, no audio.
288
  Door opened β†’ reset to start screen.
289
  """
 
 
290
  if state is None:
291
  return (narrative_text, state,
292
  gr.update(), gr.update(),
293
  gr.update(visible=True), gr.update(visible=True),
294
- gr.update(visible=False), None)
295
 
296
  # Door opened β†’ reset
297
  if state.get("door_opened"):
298
  return ("", None,
299
  gr.update(), gr.update(),
300
  gr.update(visible=True), gr.update(visible=False),
301
- gr.update(visible=False), None)
302
 
303
  room = state["room"]
304
  chain = state["chain"]
@@ -307,20 +309,20 @@ def on_continue(narrative_text, state, container_idx, key_id):
307
  msg = (narrative_text + "\n[Select a container first.]").strip()
308
  return (msg, state, gr.update(), gr.update(),
309
  gr.update(visible=False), gr.update(visible=True),
310
- gr.update(visible=False), None)
311
 
312
  combo = (container_idx, key_id if key_id else "__none__")
313
  if combo in state["tried_combos"]:
314
  msg = (narrative_text + "\n[You've already tried that.]").strip()
315
  return (msg, state, gr.update(), gr.update(),
316
  gr.update(visible=False), gr.update(visible=True),
317
- gr.update(visible=False), None)
318
 
319
- chain_entry = next((e for e in chain if e["box"] == container_idx), None)
320
  if chain_entry is None:
321
  return (narrative_text, state, gr.update(), gr.update(),
322
  gr.update(visible=False), gr.update(visible=True),
323
- gr.update(visible=False), None)
324
 
325
  required_key = chain_entry["opened_by"]
326
  right_key = (required_key is None and not key_id) or (required_key == key_id)
@@ -330,15 +332,18 @@ def on_continue(narrative_text, state, container_idx, key_id):
330
 
331
  new_stack_gallery = gr.update()
332
  new_small_img = gr.update()
333
- audio_update = gr.update(visible=False)
334
- audio_path = None
335
 
336
  if right_key:
337
  # ── Correct open: pre-generated narration + pre-baked audio ───────
338
  narration_text = (
339
  state["narrations"]["container_narrations"]
340
- .get(container_idx, "You open the container.")
341
  )
 
 
 
 
342
  if chain_entry["contains"] is not None:
343
  found_key_id = chain_entry["contains"]
344
  if found_key_id not in new_found:
@@ -369,7 +374,7 @@ def on_continue(narrative_text, state, container_idx, key_id):
369
  updated_narrative, new_state,
370
  new_stack_gallery, new_small_img,
371
  gr.update(visible=False), gr.update(visible=True),
372
- audio_update, audio_path,
373
  )
374
 
375
 
@@ -381,30 +386,34 @@ def on_open_door(state):
381
  On success: look up pre-generated narration + play pre-baked audio.
382
  No key / wrong key: pick a random static failure message, no audio.
383
  """
 
 
384
  if state is None:
385
  return ("", state, gr.update(),
386
  gr.update(visible=False), gr.update(visible=True),
387
- gr.update(visible=False), None)
388
 
389
  found = state["found_keys"]
390
 
391
  if "door_key" in found:
392
  # ── Correct key: pre-generated narration + pre-baked audio ────────
393
  narration_text = state["narrations"]["door_right"]
394
- audio_path = state["audio_paths"].get("door_right")
395
- audio_update = gr.update(value=audio_path, visible=True) if audio_path else gr.update(visible=False)
396
- new_state = {**state, "door_opened": True}
 
 
397
  return (narration_text, new_state,
398
  gr.update(visible=False),
399
  gr.update(visible=False), gr.update(visible=True),
400
- audio_update, audio_path)
401
 
402
  # ── No key or wrong key: static message, no audio ─────────────────────
403
  narration_text = random.choice(_DOOR_FAIL)
404
  return (narration_text, state,
405
  gr.update(visible=True),
406
  gr.update(visible=False), gr.update(visible=True),
407
- gr.update(visible=False), None)
408
 
409
 
410
  # ─── UI ───────────────────────────────────────────────────────────────────────
@@ -412,12 +421,13 @@ def on_open_door(state):
412
  with gr.Blocks(title="1000 Rooms") as demo:
413
 
414
  # ── Persistent state ──────────────────────────────────────────────────────
415
- room_state = gr.State(None)
416
- gen_state = gr.State(None) # text-gen output (room/chain/narrations)
417
- img_state = gr.State(None) # image-gen output (PIL images)
418
- audio_state = gr.State(None) # tts-gen output (audio_paths dict)
419
- sel_container = gr.State(-1)
420
- sel_key = gr.State("")
 
421
 
422
  gr.HTML("""
423
  <div id="nn-overlay" class="hidden">
@@ -551,7 +561,7 @@ with gr.Blocks(title="1000 Rooms") as demo:
551
  scale=1, elem_id="cont-btn",
552
  )
553
  door_btn = gr.Button(
554
- "Open Door", variant="primary",
555
  scale=1, elem_id="door-btn", visible=True,
556
  )
557
 
@@ -576,6 +586,13 @@ with gr.Blocks(title="1000 Rooms") as demo:
576
  outputs=[gen_btn, status_lbl],
577
  )
578
 
 
 
 
 
 
 
 
579
  # ── Generate wiring β€” three GPU frames chained with .then() ─────────────────
580
  #
581
  # Step 1 gen_btn.click β†’ run_text_gen (@GPU 120 s) β†’ gen_state
@@ -590,7 +607,7 @@ with gr.Blocks(title="1000 Rooms") as demo:
590
 
591
  gen_btn.click(
592
  fn=run_text_gen,
593
- inputs=[narrator_radio],
594
  outputs=[gen_state],
595
  js="""() => {
596
  const ov = document.getElementById('nn-overlay');
@@ -658,7 +675,7 @@ with gr.Blocks(title="1000 Rooms") as demo:
658
  narrative, room_state,
659
  stack_gallery, small_img,
660
  gen_screen, main_screen,
661
- narration_audio, narration_audio, # visible update + value
662
  ],
663
  )
664
 
@@ -669,7 +686,7 @@ with gr.Blocks(title="1000 Rooms") as demo:
669
  outputs=[
670
  narrative, room_state, door_btn,
671
  gen_screen, main_screen,
672
- narration_audio, narration_audio, # visible update + value
673
  ],
674
  )
675
 
 
287
  Wrong/no-key β†’ pick a random static failure message, no audio.
288
  Door opened β†’ reset to start screen.
289
  """
290
+ _no_audio = gr.update(value=None, visible=False)
291
+
292
  if state is None:
293
  return (narrative_text, state,
294
  gr.update(), gr.update(),
295
  gr.update(visible=True), gr.update(visible=True),
296
+ _no_audio)
297
 
298
  # Door opened β†’ reset
299
  if state.get("door_opened"):
300
  return ("", None,
301
  gr.update(), gr.update(),
302
  gr.update(visible=True), gr.update(visible=False),
303
+ _no_audio)
304
 
305
  room = state["room"]
306
  chain = state["chain"]
 
309
  msg = (narrative_text + "\n[Select a container first.]").strip()
310
  return (msg, state, gr.update(), gr.update(),
311
  gr.update(visible=False), gr.update(visible=True),
312
+ _no_audio)
313
 
314
  combo = (container_idx, key_id if key_id else "__none__")
315
  if combo in state["tried_combos"]:
316
  msg = (narrative_text + "\n[You've already tried that.]").strip()
317
  return (msg, state, gr.update(), gr.update(),
318
  gr.update(visible=False), gr.update(visible=True),
319
+ _no_audio)
320
 
321
+ chain_entry = next((e for e in chain if e is not None and e["box"] == container_idx), None)
322
  if chain_entry is None:
323
  return (narrative_text, state, gr.update(), gr.update(),
324
  gr.update(visible=False), gr.update(visible=True),
325
+ _no_audio)
326
 
327
  required_key = chain_entry["opened_by"]
328
  right_key = (required_key is None and not key_id) or (required_key == key_id)
 
332
 
333
  new_stack_gallery = gr.update()
334
  new_small_img = gr.update()
335
+ audio_update = _no_audio
 
336
 
337
  if right_key:
338
  # ── Correct open: pre-generated narration + pre-baked audio ───────
339
  narration_text = (
340
  state["narrations"]["container_narrations"]
341
+ .get(container_idx, "")
342
  )
343
+ # Fall back to a generic message if pre-generated text is empty
344
+ if not narration_text:
345
+ narration_text = "You open the container and look inside."
346
+
347
  if chain_entry["contains"] is not None:
348
  found_key_id = chain_entry["contains"]
349
  if found_key_id not in new_found:
 
374
  updated_narrative, new_state,
375
  new_stack_gallery, new_small_img,
376
  gr.update(visible=False), gr.update(visible=True),
377
+ audio_update,
378
  )
379
 
380
 
 
386
  On success: look up pre-generated narration + play pre-baked audio.
387
  No key / wrong key: pick a random static failure message, no audio.
388
  """
389
+ _no_audio = gr.update(value=None, visible=False)
390
+
391
  if state is None:
392
  return ("", state, gr.update(),
393
  gr.update(visible=False), gr.update(visible=True),
394
+ _no_audio)
395
 
396
  found = state["found_keys"]
397
 
398
  if "door_key" in found:
399
  # ── Correct key: pre-generated narration + pre-baked audio ────────
400
  narration_text = state["narrations"]["door_right"]
401
+ if not narration_text:
402
+ narration_text = "The door swings open. You step through into the light."
403
+ audio_path = state["audio_paths"].get("door_right")
404
+ audio_update = gr.update(value=audio_path, visible=True) if audio_path else _no_audio
405
+ new_state = {**state, "door_opened": True}
406
  return (narration_text, new_state,
407
  gr.update(visible=False),
408
  gr.update(visible=False), gr.update(visible=True),
409
+ audio_update)
410
 
411
  # ── No key or wrong key: static message, no audio ─────────────────────
412
  narration_text = random.choice(_DOOR_FAIL)
413
  return (narration_text, state,
414
  gr.update(visible=True),
415
  gr.update(visible=False), gr.update(visible=True),
416
+ _no_audio)
417
 
418
 
419
  # ─── UI ───────────────────────────────────────────────────────────────────────
 
421
  with gr.Blocks(title="1000 Rooms") as demo:
422
 
423
  # ── Persistent state ──────────────────────────────────────────────────────
424
+ room_state = gr.State(None)
425
+ gen_state = gr.State(None) # text-gen output (room/chain/narrations)
426
+ img_state = gr.State(None) # image-gen output (PIL images)
427
+ audio_state = gr.State(None) # tts-gen output (audio_paths dict)
428
+ sel_container = gr.State(-1)
429
+ sel_key = gr.State("")
430
+ narrator_state = gr.State(list(misc_f.VOICES.keys())[0]) # persists radio pick
431
 
432
  gr.HTML("""
433
  <div id="nn-overlay" class="hidden">
 
561
  scale=1, elem_id="cont-btn",
562
  )
563
  door_btn = gr.Button(
564
+ "Open Door πŸšͺ", variant="secondary",
565
  scale=1, elem_id="door-btn", visible=True,
566
  )
567
 
 
586
  outputs=[gen_btn, status_lbl],
587
  )
588
 
589
+ # ── Persist narrator selection into state so js= on gen_btn.click cannot clobber it
590
+ narrator_radio.change(
591
+ fn=lambda v: v,
592
+ inputs=[narrator_radio],
593
+ outputs=[narrator_state],
594
+ )
595
+
596
  # ── Generate wiring β€” three GPU frames chained with .then() ─────────────────
597
  #
598
  # Step 1 gen_btn.click β†’ run_text_gen (@GPU 120 s) β†’ gen_state
 
607
 
608
  gen_btn.click(
609
  fn=run_text_gen,
610
+ inputs=[narrator_state],
611
  outputs=[gen_state],
612
  js="""() => {
613
  const ov = document.getElementById('nn-overlay');
 
675
  narrative, room_state,
676
  stack_gallery, small_img,
677
  gen_screen, main_screen,
678
+ narration_audio, # single update with both value= and visible=
679
  ],
680
  )
681
 
 
686
  outputs=[
687
  narrative, room_state, door_btn,
688
  gen_screen, main_screen,
689
+ narration_audio, # single update with both value= and visible=
690
  ],
691
  )
692
 
custom_styles.py CHANGED
@@ -1,452 +1,452 @@
1
- # ─── CSS ──────────────────────────────────────────────────────────────────────
2
-
3
- CSS = """
4
- @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&display=swap');
5
-
6
- :root {
7
- --bg: #2c2720;
8
- --bg2: #3d3730;
9
- --accent: #e8a832;
10
- --glow: rgba(232,168,50,0.25);
11
- --border: #d4b896;
12
- --text: #c9b89e;
13
- --dim: #4a9f8f;
14
- --mono: 'IM Fell English', monospace;
15
- --hud: 'Cinzel Decorative';
16
- }
17
-
18
- /* ── Body & container ── */
19
- body, .gradio-container { background: var(--bg) !important; }
20
- gradio-app { background: var(--bg) !important; }
21
- .gradio-container { min-height: 100vh; }
22
-
23
- /* ── Sparks canvas – injected by JS, fixed behind everything ── */
24
- #sparks-bg {
25
- position: fixed !important;
26
- inset: 0 !important;
27
- width: 100% !important;
28
- height: 100% !important;
29
- pointer-events: none !important;
30
- z-index: 0 !important;
31
- display: block !important;
32
- }
33
-
34
- /* Lift all Gradio content above sparks */
35
- gradio-app > div,
36
- .gradio-container > .main,
37
- .contain { position: relative; z-index: 1; }
38
-
39
- /* ── Hide Gradio chrome ── */
40
- footer, .footer, .built-with, .svelte-byatnx { display: none !important; }
41
-
42
- /* ── Panels / boxes ── */
43
- .gr-panel, .gr-box, [class*="block"], .form,
44
- .gr-form, .wrap { background: transparent !important; border: none !important; box-shadow: none !important; }
45
-
46
- /* ── Text ── */
47
- label, .label-wrap span, p, h1, h2, h3 {
48
- font-family: var(--mono) !important;
49
- color: var(--text) !important;
50
- }
51
-
52
- /* ── Textbox ── */
53
- textarea, input[type=text] {
54
- background: #071e1e !important;
55
- border: 1px solid var(--border) !important;
56
- color: var(--text) !important;
57
- font-family: var(--mono) !important;
58
- border-radius: 3px !important;
59
- caret-color: var(--accent) !important;
60
- }
61
- textarea:focus, input[type=text]:focus {
62
- border-color: var(--accent) !important;
63
- outline: none !important;
64
- box-shadow: 0 0 10px var(--glow) !important;
65
- }
66
- #narrative-box textarea {
67
- height: 400px !important;
68
- min-height: 400px !important;
69
- resize: none !important;
70
- }
71
-
72
- /* ── Dropdown / listbox ── */
73
- .svelte-select, select, [class*="dropdown"] {
74
- background: #071e1e !important;
75
- border: 1px solid var(--border) !important;
76
- color: var(--text) !important;
77
- font-family: var(--mono) !important;
78
- }
79
-
80
- /* ── Buttons ── */
81
- button.primary, button[class*="primary"], .gr-button-primary {
82
- background: transparent !important;
83
- border: 1px solid var(--accent) !important;
84
- color: var(--accent) !important;
85
- font-family: var(--hud) !important;
86
- font-size: 0.72rem !important;
87
- letter-spacing: 0.2em !important;
88
- text-transform: uppercase !important;
89
- border-radius: 2px !important;
90
- transition: box-shadow 0.2s, background 0.2s !important;
91
- cursor: pointer !important;
92
- }
93
- button.primary:hover { background: rgba(0,255,204,0.07) !important; box-shadow: 0 0 16px var(--glow) !important; }
94
-
95
- button.secondary, button[class*="secondary"], .gr-button-secondary {
96
- background: transparent !important;
97
- border: 1px solid var(--border) !important;
98
- color: var(--dim) !important;
99
- font-family: var(--hud) !important;
100
- font-size: 0.72rem !important;
101
- letter-spacing: 0.18em !important;
102
- text-transform: uppercase !important;
103
- border-radius: 2px !important;
104
- }
105
-
106
- /* ── Images ── */
107
- div[data-testid="image"], .image-container, .thumbnail-item img {
108
- background: #071e1e !important;
109
- border: 1px solid var(--border) !important;
110
- border-radius: 3px !important;
111
- }
112
- .upload-container { background: #071e1e !important; }
113
-
114
- /* ── Gallery shared ── */
115
- .gallery-item, .thumbnail-item {
116
- position: relative !important;
117
- border: 2px solid var(--border) !important;
118
- border-radius: 3px !important;
119
- cursor: pointer !important;
120
- transition: border-color 0.15s, box-shadow 0.15s !important;
121
- padding: 0 !important;
122
- margin: 0 !important;
123
- overflow: hidden !important;
124
- }
125
- .gallery-item:hover { border-color: rgba(0,255,204,0.5) !important; }
126
- #top-gallery .selected-gallery,
127
- #stack-gallery .selected-gallery {
128
- border-color: var(--accent) !important;
129
- box-shadow: 0 0 16px var(--glow), inset 0 0 0 2px rgba(0,255,204,0.55) !important;
130
- }
131
- #top-gallery .selected-gallery::after,
132
- #stack-gallery .selected-gallery::after {
133
- content: "" !important;
134
- position: absolute !important;
135
- inset: 0 !important;
136
- border: 2px solid var(--accent) !important;
137
- background: rgba(0,255,204,0.12) !important;
138
- pointer-events: none !important;
139
- }
140
-
141
- /* ── Top gallery: 400Γ—400, four 200Γ—200 cells ─�� */
142
- #top-gallery { height: 400px !important; overflow: hidden !important; }
143
- #top-gallery .grid-wrap {
144
- height: 400px !important;
145
- overflow: hidden !important;
146
- padding: 0 !important;
147
- gap: 0 !important;
148
- }
149
- #top-gallery .gallery-item {
150
- width: 200px !important;
151
- height: 200px !important;
152
- }
153
- #top-gallery .thumbnail-item {
154
- width: 200px !important;
155
- height: 200px !important;
156
- }
157
- #top-gallery .thumbnail-item img {
158
- width: 200px !important;
159
- height: 200px !important;
160
- object-fit: cover !important;
161
- display: block !important;
162
- }
163
-
164
- /* ── Stack gallery: 400Γ—200, two 200Γ—200 cells ── */
165
- #stack-gallery { height: 200px !important; overflow: hidden !important; }
166
- #stack-gallery .grid-wrap {
167
- height: 200px !important;
168
- overflow: hidden !important;
169
- padding: 0 !important;
170
- gap: 0 !important;
171
- }
172
- #stack-gallery .gallery-item {
173
- width: 200px !important;
174
- height: 200px !important;
175
- }
176
- #stack-gallery .thumbnail-item {
177
- width: 200px !important;
178
- height: 200px !important;
179
- }
180
- #stack-gallery .thumbnail-item img {
181
- width: 200px !important;
182
- height: 200px !important;
183
- object-fit: cover !important;
184
- display: block !important;
185
- }
186
-
187
- /* ── CheckboxGroup as visible list ── */
188
- #listbox {
189
- background: #071e1e !important;
190
- border: 1px solid var(--border) !important;
191
- border-radius: 3px !important;
192
- padding: 4px !important;
193
- height: 200px !important;
194
- overflow-y: auto !important;
195
- }
196
- #listbox .wrap {
197
- background: transparent !important;
198
- border: none !important;
199
- display: flex !important;
200
- flex-direction: column !important;
201
- gap: 2px !important;
202
- }
203
- #listbox label {
204
- font-family: var(--mono) !important;
205
- font-size: 0.85rem !important;
206
- color: var(--text) !important;
207
- padding: 4px 8px !important;
208
- cursor: pointer !important;
209
- border-radius: 2px !important;
210
- }
211
- #listbox label:hover { background: rgba(0,255,204,0.07) !important; }
212
- #listbox input[type=checkbox] { accent-color: var(--accent) !important; }
213
-
214
- /* ── HUD label ── */
215
- .hud { font-family: var(--hud) !important; font-size: 0.55rem; letter-spacing: 0.2em;
216
- color: var(--dim) !important; text-transform: uppercase; margin-bottom: 2px; }
217
-
218
- /* ── Loading overlay ── */
219
- #nn-overlay {
220
- position: fixed; inset: 0;
221
- background: rgba(11,47,47,0.93);
222
- z-index: 9999;
223
- display: flex; flex-direction: column;
224
- align-items: center; justify-content: center;
225
- gap: 2rem;
226
- transition: opacity 0.4s;
227
- }
228
- #nn-overlay.hidden { opacity: 0; pointer-events: none; }
229
-
230
- #nn-label {
231
- font-family: var(--hud);
232
- font-size: 1rem;
233
- letter-spacing: 0.25em;
234
- color: var(--accent);
235
- animation: pulse-text 1.4s ease-in-out infinite;
236
- }
237
- @keyframes pulse-text { 0%,100%{opacity:1} 50%{opacity:0.25} }
238
-
239
- #nn-canvas { display: block; }
240
-
241
-
242
- /* ── HARD RESET spacing ── */
243
- .gradio-container {
244
- padding: 0 !important;
245
- }
246
-
247
- /* remove row gaps */
248
- .gr-row {
249
- gap: 0 !important;
250
- margin: 0 !important;
251
- }
252
-
253
- /* remove column padding */
254
- .gr-column {
255
- padding: 0 !important;
256
- margin: 0 !important;
257
- }
258
-
259
- /* kill internal block spacing */
260
- .gr-block, .gr-form, .form {
261
- margin: 0 !important;
262
- padding: 0 !important;
263
- }
264
-
265
- /* tighten gallery wrapper */
266
- #top-gallery .grid-wrap,
267
- #stack-gallery .grid-wrap {
268
- gap: 0 !important;
269
- padding: 0 !important;
270
- margin: 0 !important;
271
- }
272
-
273
- /* ensure images don't introduce spacing */
274
- #top-gallery img,
275
- #stack-gallery img {
276
- display: block !important;
277
- margin: 0 !important;
278
- }
279
-
280
- gradio-app {
281
- position: relative !important;
282
- z-index: 1 !important;
283
- }
284
-
285
- /* background canvas MUST be behind everything */
286
- #sparks-bg {
287
- position: fixed !important;
288
- inset: 0 !important;
289
- z-index: 0 !important;
290
- }
291
-
292
- /* everything UI above sparks */
293
- .gradio-container,
294
- .gradio-container > div {
295
- position: relative !important;
296
- z-index: 2 !important;
297
- }
298
-
299
- html, body {
300
- height: 100%;
301
- overflow: hidden;
302
- }
303
- """
304
-
305
- # ─── JS (head injection) ─────────────────────────────────────────────────────
306
-
307
- HEAD_JS = """
308
- <script>
309
- // ════════════════════════════════════════════════════════
310
- // Sparks – inject canvas into body after DOM ready
311
- // ════════════════════════════════════════════════════════
312
- (function waitForBody() {
313
- if (!document.body) { setTimeout(waitForBody, 50); return; }
314
-
315
- const canvas = document.createElement('canvas');
316
- canvas.id = 'sparks-bg';
317
- document.documentElement.appendChild(canvas); // append, not prepend – avoids ordering issues
318
- const ctx = canvas.getContext('2d');
319
-
320
- function resize() {
321
- canvas.width = window.innerWidth;
322
- canvas.height = window.innerHeight;
323
- canvas.style.transform = "translateZ(0)";
324
- canvas.style.willChange = "transform";
325
- }
326
- window.addEventListener('resize', resize);
327
- resize();
328
-
329
- function randSpark() {
330
- return {
331
- x: Math.random() * canvas.width,
332
- y: canvas.height + 10 + Math.random() * 30,
333
- vx: (Math.random() - 0.5) * 0.7,
334
- vy: -(0.6 + Math.random() * 2.2),
335
- life: 0.7 + Math.random() * 0.3,
336
- decay: 0.003 + Math.random() * 0.006,
337
- r: 0.8 + Math.random() * 2.2,
338
- hue: 155 + Math.random() * 50,
339
- };
340
- }
341
-
342
- const sparks = Array.from({length: 90}, randSpark);
343
-
344
- let tick = 0;
345
- function loop() {
346
- tick++;
347
- ctx.clearRect(0, 0, canvas.width, canvas.height);
348
- sparks.forEach((s, i) => {
349
- s.x += s.vx + Math.sin(tick * 0.015 + i * 0.4) * 0.35;
350
- s.y += s.vy;
351
- s.life -= s.decay;
352
- if (s.life <= 0 || s.y < -15) { sparks[i] = randSpark(); return; }
353
- ctx.save();
354
- ctx.globalAlpha = s.life * 0.9;
355
- ctx.shadowBlur = 8;
356
- ctx.shadowColor = `hsl(${s.hue},100%,65%)`;
357
- ctx.fillStyle = `hsl(${s.hue},100%,72%)`;
358
- ctx.beginPath();
359
- ctx.arc(s.x, s.y, s.r * s.life, 0, Math.PI * 2);
360
- ctx.fill();
361
- ctx.restore();
362
- });
363
- requestAnimationFrame(loop);
364
- }
365
- loop();
366
- })();
367
-
368
- // ════════════════════════════════════════════════════════
369
- // Neural-net loading canvas
370
- // ════════════════════════════════════════════════════════
371
- let _nnHandle = null;
372
-
373
- function startNN() {
374
- const cv = document.getElementById('nn-canvas');
375
- if (!cv) return;
376
- const ctx = cv.getContext('2d');
377
- cv.width = 300;
378
- cv.height = 180;
379
-
380
- const LAYERS = [2, 3, 3, 2];
381
- const W = 300, H = 180, PAD = 40;
382
- const cols = LAYERS.length;
383
-
384
- const nodes = LAYERS.map((n, ci) => {
385
- const x = PAD + ci * ((W - PAD*2) / (cols - 1));
386
- return Array.from({length: n}, (_, ni) => ({
387
- x,
388
- y: H/2 + (ni - (n-1)/2) * 44,
389
- }));
390
- });
391
-
392
- let phase = 0, phaseT = 0, last = null;
393
- const DUR = 550;
394
-
395
- function drawNN(ts) {
396
- if (!last) last = ts;
397
- const dt = ts - last; last = ts;
398
- phaseT += dt / DUR;
399
- if (phaseT >= 1) { phaseT = 0; phase = (phase + 1) % cols; }
400
-
401
- ctx.clearRect(0, 0, W, H);
402
-
403
- // wires
404
- for (let li = 0; li < cols - 1; li++) {
405
- nodes[li].forEach(a => nodes[li+1].forEach(b => {
406
- ctx.strokeStyle = '#0d3838';
407
- ctx.lineWidth = 1;
408
- ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
409
- }));
410
- }
411
-
412
- // nodes
413
- nodes.forEach((layer, li) => {
414
- layer.forEach(n => {
415
- let radius = 6, color = '#1a5f5f', glow = 0;
416
- if (li === phase) {
417
- const t = phaseT < 0.5 ? phaseT * 2 : (1 - phaseT) * 2;
418
- const e = t*t*(3-2*t);
419
- radius = 6 + e * 7;
420
- color = '#00ffcc';
421
- glow = e * 20;
422
- } else if (li < phase) {
423
- color = 'rgba(0,180,120,0.3)';
424
- }
425
- ctx.save();
426
- if (glow) { ctx.shadowBlur = glow; ctx.shadowColor = '#00ffcc'; }
427
- ctx.beginPath(); ctx.arc(n.x, n.y, radius, 0, Math.PI*2);
428
- ctx.fillStyle = color; ctx.fill();
429
- ctx.strokeStyle = li === phase ? '#00ffcc' : '#1a5f5f';
430
- ctx.lineWidth = 1.5; ctx.stroke();
431
- ctx.restore();
432
- });
433
- });
434
-
435
- _nnHandle = requestAnimationFrame(drawNN);
436
- }
437
- if (_nnHandle) cancelAnimationFrame(_nnHandle);
438
- _nnHandle = requestAnimationFrame(drawNN);
439
- }
440
-
441
- function stopNN() {
442
- if (_nnHandle) { cancelAnimationFrame(_nnHandle); _nnHandle = null; }
443
- }
444
-
445
- // ════════════════════════════════════════════════════════
446
- // Gallery selection toggle (called from onclick below)
447
- // ════════════════════════════════════════════════════════
448
- function toggleGalSel(el) {
449
- el.classList.toggle('selected-gallery');
450
- }
451
- </script>
452
  """
 
1
+ # ─── CSS ──────────────────────────────────────────────────────────────────────
2
+
3
+ CSS = """
4
+ @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&display=swap');
5
+
6
+ :root {
7
+ --bg: #2c2720;
8
+ --bg2: #3d3838;
9
+ --accent: #e8a832;
10
+ --glow: rgba(232,168,50,0.25);
11
+ --border: #d4b896;
12
+ --text: #c9b89e;
13
+ --dim: #4a9f8f;
14
+ --mono: 'IM Fell English', monospace;
15
+ --hud: 'Cinzel Decorative';
16
+ }
17
+
18
+ /* ── Body & container ── */
19
+ body, .gradio-container { background: var(--bg) !important; }
20
+ gradio-app { background: var(--bg) !important; }
21
+ .gradio-container { min-height: 100vh; }
22
+
23
+ /* ── Sparks canvas – injected by JS, fixed behind everything ── */
24
+ #sparks-bg {
25
+ position: fixed !important;
26
+ inset: 0 !important;
27
+ width: 100% !important;
28
+ height: 100% !important;
29
+ pointer-events: none !important;
30
+ z-index: 0 !important;
31
+ display: block !important;
32
+ }
33
+
34
+ /* Lift all Gradio content above sparks */
35
+ gradio-app > div,
36
+ .gradio-container > .main,
37
+ .contain { position: relative; z-index: 1; }
38
+
39
+ /* ── Hide Gradio chrome ── */
40
+ footer, .footer, .built-with, .svelte-byatnx { display: none !important; }
41
+
42
+ /* ── Panels / boxes ── */
43
+ .gr-panel, .gr-box, [class*="block"], .form,
44
+ .gr-form, .wrap { background: transparent !important; border: none !important; box-shadow: none !important; }
45
+
46
+ /* ── Text ── */
47
+ label, .label-wrap span, p, h1, h2, h3 {
48
+ font-family: var(--mono) !important;
49
+ color: var(--text) !important;
50
+ }
51
+
52
+ /* ── Textbox ── */
53
+ textarea, input[type=text] {
54
+ background: #0d3838 !important;
55
+ border: 1px solid var(--border) !important;
56
+ color: var(--text) !important;
57
+ font-family: var(--mono) !important;
58
+ border-radius: 3px !important;
59
+ caret-color: var(--accent) !important;
60
+ }
61
+ textarea:focus, input[type=text]:focus {
62
+ border-color: var(--accent) !important;
63
+ outline: none !important;
64
+ box-shadow: 0 0 10px var(--glow) !important;
65
+ }
66
+ #narrative-box textarea {
67
+ height: 400px !important;
68
+ min-height: 400px !important;
69
+ resize: none !important;
70
+ }
71
+
72
+ /* ── Dropdown / listbox ── */
73
+ .svelte-select, select, [class*="dropdown"] {
74
+ background: #071e1e !important;
75
+ border: 1px solid var(--border) !important;
76
+ color: var(--text) !important;
77
+ font-family: var(--mono) !important;
78
+ }
79
+
80
+ /* ── Buttons ── */
81
+ button.primary, button[class*="primary"], .gr-button-primary {
82
+ background: transparent !important;
83
+ border: 1px solid var(--accent) !important;
84
+ color: var(--accent) !important;
85
+ font-family: var(--hud) !important;
86
+ font-size: 0.72rem !important;
87
+ letter-spacing: 0.2em !important;
88
+ text-transform: uppercase !important;
89
+ border-radius: 2px !important;
90
+ transition: box-shadow 0.2s, background 0.2s !important;
91
+ cursor: pointer !important;
92
+ }
93
+ button.primary:hover { background: rgba(0,255,204,0.07) !important; box-shadow: 0 0 16px var(--glow) !important; }
94
+
95
+ button.secondary, button[class*="secondary"], .gr-button-secondary {
96
+ background: transparent !important;
97
+ border: 1px solid var(--border) !important;
98
+ color: var(--dim) !important;
99
+ font-family: var(--hud) !important;
100
+ font-size: 0.72rem !important;
101
+ letter-spacing: 0.18em !important;
102
+ text-transform: uppercase !important;
103
+ border-radius: 2px !important;
104
+ }
105
+
106
+ /* ── Images ── */
107
+ div[data-testid="image"], .image-container, .thumbnail-item img {
108
+ background: #071e1e !important;
109
+ border: 1px solid var(--border) !important;
110
+ border-radius: 3px !important;
111
+ }
112
+ .upload-container { background: #071e1e !important; }
113
+
114
+ /* ── Gallery shared ── */
115
+ .gallery-item, .thumbnail-item {
116
+ position: relative !important;
117
+ border: 2px solid var(--border) !important;
118
+ border-radius: 3px !important;
119
+ cursor: pointer !important;
120
+ transition: border-color 0.15s, box-shadow 0.15s !important;
121
+ padding: 0 !important;
122
+ margin: 0 !important;
123
+ overflow: hidden !important;
124
+ }
125
+ .gallery-item:hover { border-color: rgba(0,255,204,0.5) !important; }
126
+ #top-gallery .selected-gallery,
127
+ #stack-gallery .selected-gallery {
128
+ border-color: var(--accent) !important;
129
+ box-shadow: 0 0 16px var(--glow), inset 0 0 0 2px rgba(0,255,204,0.55) !important;
130
+ }
131
+ #top-gallery .selected-gallery::after,
132
+ #stack-gallery .selected-gallery::after {
133
+ content: "" !important;
134
+ position: absolute !important;
135
+ inset: 0 !important;
136
+ border: 2px solid var(--accent) !important;
137
+ background: rgba(0,255,204,0.12) !important;
138
+ pointer-events: none !important;
139
+ }
140
+
141
+ /* ── Top gallery: 400Γ—400, four 200Γ—200 cells ── */
142
+ #top-gallery { height: 400px !important; overflow: hidden !important; }
143
+ #top-gallery .grid-wrap {
144
+ height: 400px !important;
145
+ overflow: hidden !important;
146
+ padding: 0 !important;
147
+ gap: 0 !important;
148
+ }
149
+ #top-gallery .gallery-item {
150
+ width: 200px !important;
151
+ height: 200px !important;
152
+ }
153
+ #top-gallery .thumbnail-item {
154
+ width: 200px !important;
155
+ height: 200px !important;
156
+ }
157
+ #top-gallery .thumbnail-item img {
158
+ width: 200px !important;
159
+ height: 200px !important;
160
+ object-fit: cover !important;
161
+ display: block !important;
162
+ }
163
+
164
+ /* ── Stack gallery: 400Γ—200, two 200Γ—200 cells ── */
165
+ #stack-gallery { height: 200px !important; overflow: hidden !important; }
166
+ #stack-gallery .grid-wrap {
167
+ height: 200px !important;
168
+ overflow: hidden !important;
169
+ padding: 0 !important;
170
+ gap: 0 !important;
171
+ }
172
+ #stack-gallery .gallery-item {
173
+ width: 200px !important;
174
+ height: 200px !important;
175
+ }
176
+ #stack-gallery .thumbnail-item {
177
+ width: 200px !important;
178
+ height: 200px !important;
179
+ }
180
+ #stack-gallery .thumbnail-item img {
181
+ width: 200px !important;
182
+ height: 200px !important;
183
+ object-fit: cover !important;
184
+ display: block !important;
185
+ }
186
+
187
+ /* ── CheckboxGroup as visible list ── */
188
+ #listbox {
189
+ background: #071e1e !important;
190
+ border: 1px solid var(--border) !important;
191
+ border-radius: 3px !important;
192
+ padding: 4px !important;
193
+ height: 200px !important;
194
+ overflow-y: auto !important;
195
+ }
196
+ #listbox .wrap {
197
+ background: transparent !important;
198
+ border: none !important;
199
+ display: flex !important;
200
+ flex-direction: column !important;
201
+ gap: 2px !important;
202
+ }
203
+ #listbox label {
204
+ font-family: var(--mono) !important;
205
+ font-size: 0.85rem !important;
206
+ color: var(--text) !important;
207
+ padding: 4px 8px !important;
208
+ cursor: pointer !important;
209
+ border-radius: 2px !important;
210
+ }
211
+ #listbox label:hover { background: rgba(0,255,204,0.07) !important; }
212
+ #listbox input[type=checkbox] { accent-color: var(--accent) !important; }
213
+
214
+ /* ── HUD label ── */
215
+ .hud { font-family: var(--hud) !important; font-size: 0.55rem; letter-spacing: 0.2em;
216
+ color: var(--dim) !important; text-transform: uppercase; margin-bottom: 2px; }
217
+
218
+ /* ── Loading overlay ── */
219
+ #nn-overlay {
220
+ position: fixed; inset: 0;
221
+ background: #2c2720;
222
+ z-index: 9999;
223
+ display: flex; flex-direction: column;
224
+ align-items: center; justify-content: center;
225
+ gap: 2rem;
226
+ transition: opacity 0.4s;
227
+ }
228
+ #nn-overlay.hidden { opacity: 0; pointer-events: none; }
229
+
230
+ #nn-label {
231
+ font-family: var(--hud);
232
+ font-size: 1rem;
233
+ letter-spacing: 0.25em;
234
+ color: var(--accent);
235
+ animation: pulse-text 1.4s ease-in-out infinite;
236
+ }
237
+ @keyframes pulse-text { 0%,100%{opacity:1} 50%{opacity:0.25} }
238
+
239
+ #nn-canvas { display: block; }
240
+
241
+
242
+ /* ── HARD RESET spacing ── */
243
+ .gradio-container {
244
+ padding: 0 !important;
245
+ }
246
+
247
+ /* remove row gaps */
248
+ .gr-row {
249
+ gap: 0 !important;
250
+ margin: 0 !important;
251
+ }
252
+
253
+ /* remove column padding */
254
+ .gr-column {
255
+ padding: 0 !important;
256
+ margin: 0 !important;
257
+ }
258
+
259
+ /* kill internal block spacing */
260
+ .gr-block, .gr-form, .form {
261
+ margin: 0 !important;
262
+ padding: 0 !important;
263
+ }
264
+
265
+ /* tighten gallery wrapper */
266
+ #top-gallery .grid-wrap,
267
+ #stack-gallery .grid-wrap {
268
+ gap: 0 !important;
269
+ padding: 0 !important;
270
+ margin: 0 !important;
271
+ }
272
+
273
+ /* ensure images don't introduce spacing */
274
+ #top-gallery img,
275
+ #stack-gallery img {
276
+ display: block !important;
277
+ margin: 0 !important;
278
+ }
279
+
280
+ gradio-app {
281
+ position: relative !important;
282
+ z-index: 1 !important;
283
+ }
284
+
285
+ /* background canvas MUST be behind everything */
286
+ #sparks-bg {
287
+ position: fixed !important;
288
+ inset: 0 !important;
289
+ z-index: 0 !important;
290
+ }
291
+
292
+ /* everything UI above sparks */
293
+ .gradio-container,
294
+ .gradio-container > div {
295
+ position: relative !important;
296
+ z-index: 2 !important;
297
+ }
298
+
299
+ html, body {
300
+ height: 100%;
301
+ overflow: hidden;
302
+ }
303
+ """
304
+
305
+ # ─── JS (head injection) ─────────────────────────────────────────────────────
306
+
307
+ HEAD_JS = """
308
+ <script>
309
+ // ════════════════════════════════════════════════════════
310
+ // Sparks – inject canvas into body after DOM ready
311
+ // ════════════════════════════════════════════════════════
312
+ (function waitForBody() {
313
+ if (!document.body) { setTimeout(waitForBody, 50); return; }
314
+
315
+ const canvas = document.createElement('canvas');
316
+ canvas.id = 'sparks-bg';
317
+ document.documentElement.appendChild(canvas); // append, not prepend – avoids ordering issues
318
+ const ctx = canvas.getContext('2d');
319
+
320
+ function resize() {
321
+ canvas.width = window.innerWidth;
322
+ canvas.height = window.innerHeight;
323
+ canvas.style.transform = "translateZ(0)";
324
+ canvas.style.willChange = "transform";
325
+ }
326
+ window.addEventListener('resize', resize);
327
+ resize();
328
+
329
+ function randSpark() {
330
+ return {
331
+ x: Math.random() * canvas.width,
332
+ y: canvas.height + 10 + Math.random() * 30,
333
+ vx: (Math.random() - 0.5) * 0.7,
334
+ vy: -(0.6 + Math.random() * 2.2),
335
+ life: 0.7 + Math.random() * 0.3,
336
+ decay: 0.003 + Math.random() * 0.006,
337
+ r: 0.8 + Math.random() * 2.2,
338
+ hue: 30 + Math.random() * 20,
339
+ };
340
+ }
341
+
342
+ const sparks = Array.from({length: 90}, randSpark);
343
+
344
+ let tick = 0;
345
+ function loop() {
346
+ tick++;
347
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
348
+ sparks.forEach((s, i) => {
349
+ s.x += s.vx + Math.sin(tick * 0.015 + i * 0.4) * 0.35;
350
+ s.y += s.vy;
351
+ s.life -= s.decay;
352
+ if (s.life <= 0 || s.y < -15) { sparks[i] = randSpark(); return; }
353
+ ctx.save();
354
+ ctx.globalAlpha = s.life * 0.9;
355
+ ctx.shadowBlur = 8;
356
+ ctx.shadowColor = `hsl(${s.hue},100%,65%)`;
357
+ ctx.fillStyle = `hsl(${s.hue},100%,72%)`;
358
+ ctx.beginPath();
359
+ ctx.arc(s.x, s.y, s.r * s.life, 0, Math.PI * 2);
360
+ ctx.fill();
361
+ ctx.restore();
362
+ });
363
+ requestAnimationFrame(loop);
364
+ }
365
+ loop();
366
+ })();
367
+
368
+ // ════════════════════════════════════════════════════════
369
+ // Neural-net loading canvas
370
+ // ════════════════════════════════════════════════════════
371
+ let _nnHandle = null;
372
+
373
+ function startNN() {
374
+ const cv = document.getElementById('nn-canvas');
375
+ if (!cv) return;
376
+ const ctx = cv.getContext('2d');
377
+ cv.width = 300;
378
+ cv.height = 180;
379
+
380
+ const LAYERS = [2, 3, 3, 2];
381
+ const W = 300, H = 180, PAD = 40;
382
+ const cols = LAYERS.length;
383
+
384
+ const nodes = LAYERS.map((n, ci) => {
385
+ const x = PAD + ci * ((W - PAD*2) / (cols - 1));
386
+ return Array.from({length: n}, (_, ni) => ({
387
+ x,
388
+ y: H/2 + (ni - (n-1)/2) * 44,
389
+ }));
390
+ });
391
+
392
+ let phase = 0, phaseT = 0, last = null;
393
+ const DUR = 550;
394
+
395
+ function drawNN(ts) {
396
+ if (!last) last = ts;
397
+ const dt = ts - last; last = ts;
398
+ phaseT += dt / DUR;
399
+ if (phaseT >= 1) { phaseT = 0; phase = (phase + 1) % cols; }
400
+
401
+ ctx.clearRect(0, 0, W, H);
402
+
403
+ // wires
404
+ for (let li = 0; li < cols - 1; li++) {
405
+ nodes[li].forEach(a => nodes[li+1].forEach(b => {
406
+ ctx.strokeStyle = '#3a2e1a';
407
+ ctx.lineWidth = 1;
408
+ ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
409
+ }));
410
+ }
411
+
412
+ // nodes
413
+ nodes.forEach((layer, li) => {
414
+ layer.forEach(n => {
415
+ let radius = 6, color = '#c47c1a', glow = 0;
416
+ if (li === phase) {
417
+ const t = phaseT < 0.5 ? phaseT * 2 : (1 - phaseT) * 2;
418
+ const e = t*t*(3-2*t);
419
+ radius = 6 + e * 7;
420
+ color = '#e8a832';
421
+ glow = e * 20;
422
+ } else if (li < phase) {
423
+ color = 'rgba(196,124,26,0.3)';
424
+ }
425
+ ctx.save();
426
+ if (glow) { ctx.shadowBlur = glow; ctx.shadowColor = '#e8a832'; }
427
+ ctx.beginPath(); ctx.arc(n.x, n.y, radius, 0, Math.PI*2);
428
+ ctx.fillStyle = color; ctx.fill();
429
+ ctx.strokeStyle = li === phase ? '#e8a832' : '#c47c1a';
430
+ ctx.lineWidth = 1.5; ctx.stroke();
431
+ ctx.restore();
432
+ });
433
+ });
434
+
435
+ _nnHandle = requestAnimationFrame(drawNN);
436
+ }
437
+ if (_nnHandle) cancelAnimationFrame(_nnHandle);
438
+ _nnHandle = requestAnimationFrame(drawNN);
439
+ }
440
+
441
+ function stopNN() {
442
+ if (_nnHandle) { cancelAnimationFrame(_nnHandle); _nnHandle = null; }
443
+ }
444
+
445
+ // ════════════════════════════════════════════════════════
446
+ // Gallery selection toggle (called from onclick below)
447
+ // ════════════════════════════════════════════════════════
448
+ function toggleGalSel(el) {
449
+ el.classList.toggle('selected-gallery');
450
+ }
451
+ </script>
452
  """