AI-nthusiast commited on
Commit
63ce055
·
1 Parent(s): 18187e6

Fix spaces import and add requirements

Browse files
Files changed (2) hide show
  1. app.py +580 -4
  2. requirements.txt +8 -0
app.py CHANGED
@@ -1,7 +1,583 @@
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
 
 
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cognitive Proxy - Brain-Steered Language Model
3
+ Hugging Face Spaces deployment
4
+ Author: Sandro Andric
5
+ """
6
+
7
  import gradio as gr
8
+ import torch
9
+ import torch.nn as nn
10
+ import numpy as np
11
+ import pickle
12
+ import os
13
+ from pathlib import Path
14
+ from sklearn.decomposition import PCA
15
+ from transformers import AutoTokenizer, AutoModelForCausalLM
16
+ import plotly.graph_objects as go
17
+ import spaces # For ZeroGPU on Hugging Face
18
+
19
+ # --- CONFIG ---
20
+ import os
21
+ from pathlib import Path
22
+
23
+ # Get the directory of this script
24
+ SCRIPT_DIR = Path(__file__).parent if __file__ else Path.cwd()
25
+
26
+ # Try multiple possible locations for the model files
27
+ if (SCRIPT_DIR / "results" / "final_atlas_256_vocab.pkl").exists():
28
+ ATLAS_PATH = str(SCRIPT_DIR / "results" / "final_atlas_256_vocab.pkl")
29
+ ADAPTER_PATH = str(SCRIPT_DIR / "results" / "tinyllama_adapter_direct.pt")
30
+ elif (SCRIPT_DIR / "final_atlas_256_vocab.pkl").exists():
31
+ ATLAS_PATH = str(SCRIPT_DIR / "final_atlas_256_vocab.pkl")
32
+ ADAPTER_PATH = str(SCRIPT_DIR / "tinyllama_adapter_direct.pt")
33
+ else:
34
+ # Fallback to expected location
35
+ ATLAS_PATH = "results/final_atlas_256_vocab.pkl"
36
+ ADAPTER_PATH = "results/tinyllama_adapter_direct.pt"
37
+
38
+ print(f"Atlas path: {ATLAS_PATH}")
39
+ print(f"Adapter path: {ADAPTER_PATH}")
40
+
41
+ MODEL_ID = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
42
+
43
+ # --- ADAPTER CLASS ---
44
+ class TinyLlamaAdapterDirect(nn.Module):
45
+ def __init__(self, input_dim=2048, hidden_dim=1024, output_dim=65536):
46
+ super().__init__()
47
+ self.net = nn.Sequential(
48
+ nn.Linear(input_dim, hidden_dim),
49
+ nn.LayerNorm(hidden_dim),
50
+ nn.GELU(),
51
+ nn.Dropout(0.1),
52
+ nn.Linear(hidden_dim, hidden_dim),
53
+ nn.LayerNorm(hidden_dim),
54
+ nn.GELU(),
55
+ nn.Dropout(0.1),
56
+ nn.Linear(hidden_dim, hidden_dim // 2),
57
+ nn.LayerNorm(hidden_dim // 2),
58
+ nn.GELU(),
59
+ nn.Linear(hidden_dim // 2, output_dim),
60
+ )
61
+
62
+ def forward(self, x):
63
+ return self.net(x)
64
+
65
+ # Global system cache
66
+ system = None
67
+
68
+ def load_system():
69
+ global system
70
+ if system is not None:
71
+ return system
72
+
73
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
74
+
75
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
76
+ tokenizer.pad_token = tokenizer.eos_token
77
+
78
+ # Use float32 for CPU, float16 for GPU
79
+ dtype = torch.float16 if torch.cuda.is_available() else torch.float32
80
+ try:
81
+ # Try new parameter name first
82
+ model = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype=dtype).to(device)
83
+ except TypeError:
84
+ # Fall back to old parameter name
85
+ model = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype=dtype).to(device)
86
+ model.eval()
87
+
88
+ adapter = TinyLlamaAdapterDirect().to(device).to(dtype)
89
+ if os.path.exists(ADAPTER_PATH):
90
+ adapter.load_state_dict(torch.load(ADAPTER_PATH, map_location=device, weights_only=True))
91
+ adapter.eval()
92
+
93
+ if os.path.exists(ATLAS_PATH):
94
+ print(f"Loading atlas from {ATLAS_PATH}")
95
+ with open(ATLAS_PATH, 'rb') as f:
96
+ data = pickle.load(f)
97
+ if isinstance(data, dict):
98
+ print(f"Atlas data keys: {list(data.keys())[:5]}")
99
+ if 'means' in data:
100
+ atlas = data['means']
101
+ print(f"Using 'means' key, got {len(atlas) if isinstance(atlas, dict) else 'not a dict'} items")
102
+ else:
103
+ atlas = data
104
+ print(f"Using data directly, got {len(atlas) if isinstance(atlas, dict) else 'not a dict'} items")
105
+ else:
106
+ atlas = data
107
+ print(f"Atlas is not a dict, type: {type(data)}")
108
+ else:
109
+ print(f"Atlas file not found at {ATLAS_PATH}")
110
+ atlas = {}
111
+
112
+ # Ensure atlas is valid
113
+ if not atlas or not isinstance(atlas, dict):
114
+ print(f"Warning: Atlas is empty or invalid, using fallback")
115
+ atlas = {'word1': np.random.randn(256, 256), 'word2': np.random.randn(256, 256)}
116
+
117
+ words = list(atlas.keys())
118
+ print(f"Loaded atlas with {len(words)} words")
119
+ if len(words) < 2:
120
+ print(f"Warning: Not enough words in atlas ({len(words)}), using fallback")
121
+ atlas = {'word1': np.random.randn(256, 256), 'word2': np.random.randn(256, 256)}
122
+ words = list(atlas.keys())
123
+
124
+ # Handle both 256x256 and flat arrays
125
+ first_val = np.array(atlas[words[0]])
126
+ if first_val.shape == (256, 256):
127
+ plv_matrix = np.array([np.array(atlas[w]).flatten() for w in words])
128
+ else:
129
+ plv_matrix = np.array([np.array(atlas[w]) for w in words])
130
+
131
+ # Ensure matrix is 2D
132
+ if len(plv_matrix.shape) == 1 or plv_matrix.shape[0] < 2:
133
+ print(f"Warning: Invalid PLV matrix shape {plv_matrix.shape}, using fallback")
134
+ plv_matrix = np.random.randn(10, 65536)
135
+
136
+ pca = PCA(n_components=min(10, plv_matrix.shape[0] - 1))
137
+ pca.fit(plv_matrix)
138
+ pc1_axis = pca.components_[0]
139
+ pc1_axis = pc1_axis / np.linalg.norm(pc1_axis)
140
+ global_mean = plv_matrix.mean(axis=0)
141
+
142
+ system = {
143
+ 'model': model,
144
+ 'tokenizer': tokenizer,
145
+ 'adapter': adapter,
146
+ 'axis': torch.tensor(pc1_axis, dtype=torch.float32).to(device),
147
+ 'global_mean': torch.tensor(global_mean, dtype=torch.float32).to(device),
148
+ 'device': device
149
+ }
150
+ return system
151
+
152
+ @spaces.GPU(duration=60)
153
+ def generate_variants(prompt, scenario, max_tokens):
154
+ """Generate all three variants"""
155
+ sys = load_system()
156
+
157
+ if scenario == "Educational":
158
+ prompt_formatted = f"<|user|>\n{prompt}\n<|assistant|>\n"
159
+ alpha_strength = 5.0
160
+ elif scenario == "Technical writing":
161
+ prompt_formatted = f"<|user|>\n{prompt}\n<|assistant|>\n"
162
+ alpha_strength = 5.0
163
+ else:
164
+ prompt_formatted = prompt
165
+ alpha_strength = 3.0
166
+
167
+ outputs = []
168
+ for alpha in [-alpha_strength, 0, alpha_strength]:
169
+ inputs = sys['tokenizer'](prompt_formatted, return_tensors='pt').to(sys['device'])
170
+ generated_ids = inputs.input_ids.clone()
171
+
172
+ for _ in range(max_tokens):
173
+ outputs_model = sys['model'](generated_ids, output_hidden_states=True)
174
+ hidden = outputs_model.hidden_states[-1][:, -1, :]
175
+
176
+ # Ensure proper dtype for adapter
177
+ adapter_dtype = next(sys['adapter'].parameters()).dtype
178
+ hidden = hidden.to(adapter_dtype)
179
+
180
+ if alpha != 0:
181
+ hidden = hidden.detach().requires_grad_(True)
182
+ plv_pred = sys['adapter'](hidden)
183
+ score = torch.sum(plv_pred * sys['axis'].to(adapter_dtype))
184
+ grad = torch.autograd.grad(score, hidden, retain_graph=False)[0]
185
+ grad = grad / (grad.norm() + 1e-8)
186
+ hidden = hidden.detach() + alpha * grad.detach()
187
+
188
+ with torch.no_grad():
189
+ logits = sys['model'].lm_head(sys['model'].model.norm(hidden))
190
+ probs = torch.softmax(logits / 0.8, dim=-1)
191
+ next_token = torch.multinomial(probs, num_samples=1)
192
+ generated_ids = torch.cat([generated_ids, next_token], dim=-1)
193
+ if next_token.item() == sys['tokenizer'].eos_token_id:
194
+ break
195
+
196
+ text = sys['tokenizer'].decode(generated_ids[0], skip_special_tokens=True)
197
+ if "<|assistant|>" in text:
198
+ text = text.split("<|assistant|>")[-1].strip()
199
+ outputs.append(text)
200
+
201
+ return outputs[0], outputs[1], outputs[2]
202
+
203
+ @spaces.GPU(duration=30)
204
+ def analyze_text(text):
205
+ """Analyze text and return score with visualization"""
206
+ sys = load_system()
207
+
208
+ with torch.no_grad():
209
+ inputs = sys['tokenizer'](text, return_tensors='pt').to(sys['device'])
210
+ out = sys['model'](**inputs, output_hidden_states=True)
211
+ last_hidden = out.hidden_states[-1][0, -1, :]
212
+ # Ensure proper dtype for adapter
213
+ adapter_dtype = next(sys['adapter'].parameters()).dtype
214
+ last_hidden = last_hidden.to(adapter_dtype)
215
+ plv_pred = sys['adapter'](last_hidden.unsqueeze(0))
216
+ plv_flat = plv_pred[0]
217
+ plv_centered = plv_flat - sys['global_mean'].to(adapter_dtype)
218
+ score = (plv_centered * sys['axis'].to(adapter_dtype)).sum().item()
219
+
220
+ # Create minimal gauge like Streamlit
221
+ gauge_min = min(-300, score - 50)
222
+ gauge_max = max(300, score + 50)
223
+
224
+ fig = go.Figure(go.Indicator(
225
+ mode="number+gauge",
226
+ value=score,
227
+ gauge={
228
+ 'shape': "angular",
229
+ 'axis': {'range': [gauge_min, gauge_max], 'tickwidth': 0.5, 'tickcolor': '#ccc'},
230
+ 'bar': {'color': "#333", 'thickness': 0.15},
231
+ 'bgcolor': "white",
232
+ 'borderwidth': 1,
233
+ 'bordercolor': "#e0e0e0",
234
+ 'steps': [
235
+ {'range': [gauge_min, -5], 'color': "#e8f5e9"},
236
+ {'range': [-5, 5], 'color': "#fafafa"},
237
+ {'range': [5, gauge_max], 'color': "#fff3e0"}
238
+ ],
239
+ },
240
+ number={'font': {'size': 36, 'color': '#000'}}
241
+ ))
242
+
243
+ fig.update_layout(
244
+ height=250,
245
+ margin={'l': 20, 'r': 20, 't': 40, 'b': 20},
246
+ paper_bgcolor='rgba(0,0,0,0)',
247
+ font={'color': '#666'}
248
+ )
249
+
250
+ if score > 5:
251
+ interpretation = "**Syntactic dominance** \nText patterns match brain activity during grammatical processing"
252
+ elif score < -5:
253
+ interpretation = "**Semantic dominance** \nText patterns match brain activity during meaning comprehension"
254
+ else:
255
+ interpretation = "**Balanced** \nMixed patterns - both structure and meaning equally present"
256
+
257
+ return fig, interpretation, score
258
+
259
+ @spaces.GPU(duration=60)
260
+ def generate_steered(prompt, alpha, max_tokens):
261
+ """Generate with custom steering"""
262
+ sys = load_system()
263
+
264
+ inputs = sys['tokenizer'](prompt, return_tensors='pt').to(sys['device'])
265
+ generated_ids = inputs.input_ids.clone()
266
+
267
+ for _ in range(max_tokens):
268
+ outputs_model = sys['model'](generated_ids, output_hidden_states=True)
269
+ hidden = outputs_model.hidden_states[-1][:, -1, :]
270
+
271
+ # Ensure proper dtype for adapter
272
+ adapter_dtype = next(sys['adapter'].parameters()).dtype
273
+ hidden = hidden.to(adapter_dtype)
274
+
275
+ if alpha != 0:
276
+ hidden = hidden.detach().requires_grad_(True)
277
+ plv_pred = sys['adapter'](hidden)
278
+ score = torch.sum(plv_pred * sys['axis'].to(adapter_dtype))
279
+ grad = torch.autograd.grad(score, hidden, retain_graph=False)[0]
280
+ grad = grad / (grad.norm() + 1e-8)
281
+ hidden = hidden.detach() + alpha * grad.detach()
282
+
283
+ with torch.no_grad():
284
+ logits = sys['model'].lm_head(sys['model'].model.norm(hidden))
285
+ probs = torch.softmax(logits / 0.8, dim=-1)
286
+ next_token = torch.multinomial(probs, num_samples=1)
287
+ generated_ids = torch.cat([generated_ids, next_token], dim=-1)
288
+ if next_token.item() == sys['tokenizer'].eos_token_id:
289
+ break
290
+
291
+ return sys['tokenizer'].decode(generated_ids[0], skip_special_tokens=True)
292
+
293
+ # Custom CSS to match Streamlit minimal design
294
+ custom_css = """
295
+ <style>
296
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap');
297
+
298
+ /* Global font */
299
+ .gradio-container, .gradio-container * {
300
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
301
+ }
302
+
303
+ /* Clean header */
304
+ .main-header {
305
+ font-size: 14px;
306
+ font-weight: 300;
307
+ letter-spacing: 2px;
308
+ text-transform: uppercase;
309
+ color: #666;
310
+ margin-bottom: 8px;
311
+ }
312
+
313
+ .main-title {
314
+ font-size: 48px;
315
+ font-weight: 300;
316
+ line-height: 1.1;
317
+ letter-spacing: -1px;
318
+ margin-bottom: 16px;
319
+ }
320
+
321
+ .subtitle {
322
+ font-size: 18px;
323
+ font-weight: 300;
324
+ color: #666;
325
+ line-height: 1.6;
326
+ }
327
+
328
+ /* Clean tabs like Streamlit */
329
+ .tabs {
330
+ border-bottom: 1px solid #e0e0e0 !important;
331
+ }
332
+
333
+ .tab-nav button {
334
+ background: none !important;
335
+ border: none !important;
336
+ border-bottom: 2px solid transparent !important;
337
+ color: #666 !important;
338
+ font-weight: 400 !important;
339
+ font-size: 14px !important;
340
+ padding: 8px 16px !important;
341
+ text-transform: none !important;
342
+ }
343
+
344
+ .tab-nav button.selected {
345
+ color: #000 !important;
346
+ border-bottom-color: #000 !important;
347
+ }
348
+
349
+ /* Minimal buttons */
350
+ button.primary {
351
+ background: white !important;
352
+ border: 1px solid #000 !important;
353
+ color: #000 !important;
354
+ font-weight: 400 !important;
355
+ padding: 10px 20px !important;
356
+ transition: all 0.2s !important;
357
+ }
358
+
359
+ button.primary:hover {
360
+ background: #000 !important;
361
+ color: white !important;
362
+ }
363
+
364
+ /* Clean textboxes */
365
+ textarea, input[type="text"] {
366
+ border: 1px solid #e0e0e0 !important;
367
+ border-radius: 0 !important;
368
+ font-size: 14px !important;
369
+ }
370
+
371
+ /* Section titles */
372
+ .section-title {
373
+ font-size: 11px;
374
+ font-weight: 500;
375
+ letter-spacing: 1.5px;
376
+ text-transform: uppercase;
377
+ color: #999;
378
+ margin: 24px 0 16px 0;
379
+ }
380
+
381
+ /* Value labels */
382
+ .value-label {
383
+ font-size: 12px;
384
+ color: #999;
385
+ margin-bottom: 4px;
386
+ }
387
+
388
+ /* Remove gradio branding */
389
+ footer { display: none !important; }
390
+ .dark { display: none !important; }
391
+ </style>
392
+ """
393
+
394
+ # Create interface
395
+ with gr.Blocks(title="Cognitive Proxy") as demo:
396
+
397
+ # Header
398
+ gr.HTML("""
399
+ <div>
400
+ <div class="main-header">Neural Language Interface</div>
401
+ <div class="main-title">Cognitive Proxy</div>
402
+ <div class="subtitle">Steering language models through brain-derived coordinate spaces.<br>
403
+ Using MEG phase-locking patterns from 21 subjects as control geometry.</div>
404
+ <div style="color: #999; font-size: 13px; margin-top: 16px;">Sandro Andric</div>
405
+ </div>
406
+ """)
407
+
408
+ # How it works expander
409
+ with gr.Accordion("How this works", open=False):
410
+ gr.Markdown("""
411
+ **What makes this special:** This AI is controlled by real human brain data.
412
+ We recorded brain activity from 21 people listening to stories, discovered how their brains organize language,
413
+ and now use those patterns to steer what the AI generates.
414
+
415
+ **Try this:**
416
+ 1. Start with the **Compare** tab and choose **Educational**
417
+ 2. Click "Generate all variants" to see three versions side by side
418
+ 3. Notice how the left (concrete) version uses analogies while the right (abstract) uses logic
419
+ 4. The difference comes from steering along brain axes discovered from MEG recordings
420
+
421
+ **The science:** Different brain regions activate for grammar vs meaning.
422
+ We project the AI's internal states into this brain coordinate system and steer along the axis.
423
+ """)
424
+
425
+ with gr.Tabs():
426
+ # Compare Tab
427
+ with gr.TabItem("Compare"):
428
+ gr.HTML('<div class="section-title">Comparative Analysis</div>')
429
+
430
+ gr.Markdown("""
431
+ See how brain steering affects AI output. Try **Educational** to see the difference between
432
+ abstract explanations vs concrete analogies, or **Technical writing** to compare formal vs friendly tones.
433
+ All controlled by brain patterns from 21 human subjects.
434
+ """)
435
+
436
+ with gr.Row():
437
+ scenario = gr.Dropdown(
438
+ choices=["Educational", "Technical writing", "Free form"],
439
+ value="Educational",
440
+ label="Scenario",
441
+ container=False
442
+ )
443
+
444
+ prompt = gr.Textbox(
445
+ value="Explain quantum entanglement in simple terms.",
446
+ label="",
447
+ placeholder="Enter your prompt...",
448
+ lines=4
449
+ )
450
+
451
+ with gr.Row():
452
+ max_tokens = gr.Slider(20, 150, 80, label="Max tokens", container=False)
453
+ generate_btn = gr.Button("Generate all variants", variant="primary")
454
+
455
+ gr.HTML('<div style="margin-top: 24px;"></div>')
456
+
457
+ with gr.Row():
458
+ with gr.Column():
459
+ gr.HTML('<div class="value-label">Concrete / Analogies</div>')
460
+ output_semantic = gr.Textbox(
461
+ label="",
462
+ lines=10,
463
+ interactive=False,
464
+ container=False
465
+ )
466
+ gr.Markdown("*Steered toward meaning patterns*", elem_classes=["caption"])
467
+
468
+ with gr.Column():
469
+ gr.HTML('<div class="value-label">Baseline</div>')
470
+ output_baseline = gr.Textbox(
471
+ label="",
472
+ lines=10,
473
+ interactive=False,
474
+ container=False
475
+ )
476
+ gr.Markdown("*No brain steering*", elem_classes=["caption"])
477
+
478
+ with gr.Column():
479
+ gr.HTML('<div class="value-label">Abstract / Logical</div>')
480
+ output_syntactic = gr.Textbox(
481
+ label="",
482
+ lines=10,
483
+ interactive=False,
484
+ container=False
485
+ )
486
+ gr.Markdown("*Steered toward structure patterns*", elem_classes=["caption"])
487
+
488
+ generate_btn.click(
489
+ generate_variants,
490
+ inputs=[prompt, scenario, max_tokens],
491
+ outputs=[output_semantic, output_baseline, output_syntactic]
492
+ )
493
+
494
+ # Inspect Tab
495
+ with gr.TabItem("Inspect"):
496
+ gr.HTML('<div class="section-title">Brain Space Projection</div>')
497
+
498
+ gr.Markdown("""
499
+ Enter any text to see how it aligns with brain patterns. The meter shows whether your text
500
+ activates brain regions associated with grammar/structure (positive) or meaning/content (negative).
501
+ """)
502
+
503
+ with gr.Row():
504
+ with gr.Column():
505
+ text_input = gr.Textbox(
506
+ value="The scientist discovered",
507
+ label="",
508
+ placeholder="Enter text to analyze...",
509
+ lines=6
510
+ )
511
+ analyze_btn = gr.Button("Project", variant="primary")
512
+
513
+ with gr.Column():
514
+ gauge_plot = gr.Plot(label="")
515
+ interpretation = gr.Markdown("")
516
+
517
+ with gr.Accordion("What the number means", open=False):
518
+ gr.Markdown("""
519
+ - **Negative values (green)** = semantic/meaning focus
520
+ - **Positive values (amber)** = syntactic/grammar focus
521
+ - **Larger magnitude** = stronger pattern
522
+ - **Range** typically -300 to +300
523
+ """)
524
+
525
+ def analyze_text_wrapper(text):
526
+ fig, interp, _ = analyze_text(text) # Ignore the score
527
+ return fig, interp
528
+
529
+ analyze_btn.click(
530
+ analyze_text_wrapper,
531
+ inputs=[text_input],
532
+ outputs=[gauge_plot, interpretation]
533
+ )
534
+
535
+ # Steer Tab
536
+ with gr.TabItem("Steer"):
537
+ gr.HTML('<div class="section-title">Neural Steering</div>')
538
+
539
+ with gr.Row():
540
+ with gr.Column(scale=2):
541
+ prompt_steer = gr.Textbox(
542
+ value="The scientist discovered",
543
+ label="",
544
+ placeholder="Enter prompt...",
545
+ lines=5
546
+ )
547
+
548
+ with gr.Column(scale=1):
549
+ gr.HTML('<div class="value-label">Tokens</div>')
550
+ tokens_steer = gr.Slider(20, 150, 60, label="", container=False)
551
+
552
+ gr.HTML('<div class="value-label">Alpha</div>')
553
+ alpha_steer = gr.Slider(-5.0, 5.0, 0.0, 0.5, label="", container=False)
554
+ gr.Markdown("*negative → semantic | positive → syntactic*", elem_classes=["caption"])
555
+
556
+ steer_btn = gr.Button("Generate", variant="primary")
557
+
558
+ gr.HTML('<div class="section-title">Output</div>')
559
+ output_steer = gr.Textbox(label="", lines=8, interactive=False, container=False)
560
+
561
+ steer_btn.click(
562
+ generate_steered,
563
+ inputs=[prompt_steer, alpha_steer, tokens_steer],
564
+ outputs=[output_steer]
565
+ )
566
 
567
+ # Footer
568
+ gr.HTML("""
569
+ <div style="text-align: center; color: #999; font-size: 12px; padding: 40px 0 20px 0; border-top: 1px solid #e0e0e0; margin-top: 40px;">
570
+ © 2025 Sandro Andric | <a href="https://ainthusiast.com" style="color: #999;">Ainthusiast.com</a>
571
+ </div>
572
+ """)
573
 
574
+ demo.launch(
575
+ theme=gr.themes.Base(
576
+ primary_hue="gray",
577
+ neutral_hue="gray",
578
+ text_size="md",
579
+ spacing_size="lg",
580
+ radius_size="none",
581
+ ),
582
+ css=custom_css
583
+ )
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio==4.19.2
2
+ torch==2.1.0
3
+ transformers==4.36.0
4
+ numpy==1.24.3
5
+ scikit-learn==1.3.0
6
+ plotly==5.18.0
7
+ spaces==0.19.4
8
+ accelerate==0.25.0