OliverPerrin commited on
Commit
fc64ea0
ยท
1 Parent(s): 38eb401

Redesign Gradio demo with book/news browsing, update .gitignore

Browse files
Files changed (3) hide show
  1. .gitignore +4 -0
  2. scripts/demo_gradio.py +457 -189
  3. src/training/safe_compile.py +5 -2
.gitignore CHANGED
@@ -64,3 +64,7 @@ ehthumbs_vista.db
64
  # Config overrides
65
  configs/local/*.png
66
  *.pt
 
 
 
 
 
64
  # Config overrides
65
  configs/local/*.png
66
  *.pt
67
+
68
+ # Backup/private files
69
+ scripts/demo_gradio_old.py
70
+ docs/paper.tex
scripts/demo_gradio.py CHANGED
@@ -1,20 +1,23 @@
1
  """
2
  Gradio demo for LexiMind multi-task NLP model.
3
 
4
- Showcases the model's capabilities across three tasks:
5
- - Summarization: Generates concise summaries of input text
6
- - Emotion Detection: Multi-label emotion classification
7
- - Topic Classification: Categorizes text into topics
 
8
 
9
  Author: Oliver Perrin
10
- Date: 2025-12-05
11
  """
12
 
13
  from __future__ import annotations
14
 
15
  import json
 
16
  import sys
17
  from pathlib import Path
 
18
 
19
  import gradio as gr
20
 
@@ -37,14 +40,115 @@ logger = get_logger(__name__)
37
  # --------------- Constants ---------------
38
 
39
  OUTPUTS_DIR = PROJECT_ROOT / "outputs"
 
 
 
 
40
  EVAL_REPORT_PATH = OUTPUTS_DIR / "evaluation_report.json"
41
  TRAINING_HISTORY_PATH = OUTPUTS_DIR / "training_history.json"
42
 
43
- SAMPLE_TEXTS = [
44
- "Global markets tumbled today as investors reacted to rising inflation concerns. The Federal Reserve hinted at potential interest rate hikes, sending shockwaves through technology and banking sectors. Analysts predict continued volatility as economic uncertainty persists.",
45
- "Scientists at MIT have developed a breakthrough quantum computing chip that operates at room temperature. This advancement could revolutionize drug discovery, cryptography, and artificial intelligence. The research team published their findings in Nature.",
46
- "The championship game ended in dramatic fashion as the underdog team scored in the final seconds to secure victory. Fans rushed the field in celebration, marking the team's first title in 25 years.",
47
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  # --------------- Pipeline Management ---------------
50
 
@@ -76,68 +180,38 @@ def get_pipeline():
76
  return _pipeline
77
 
78
 
79
- # --------------- Core Functions ---------------
80
 
81
 
82
- def analyze(text: str) -> tuple[str, str, str]:
83
  """Run all three tasks and return formatted results."""
84
  if not text or not text.strip():
85
- return "Please enter text above to analyze.", "", ""
86
 
87
  try:
88
  pipe = get_pipeline()
89
 
90
  # Run tasks
91
- summary = pipe.summarize([text], max_length=128)[0].strip()
92
  if not summary:
93
  summary = "(Unable to generate summary)"
94
 
95
- emotions = pipe.predict_emotions([text], threshold=0.3)[0] # Lower threshold
96
  topic = pipe.predict_topics([text])[0]
97
 
98
- # Format emotions with emoji
99
- emotion_emoji = {
100
- "joy": "๐Ÿ˜Š",
101
- "love": "โค๏ธ",
102
- "anger": "๐Ÿ˜ ",
103
- "fear": "๐Ÿ˜จ",
104
- "sadness": "๐Ÿ˜ข",
105
- "surprise": "๐Ÿ˜ฒ",
106
- "neutral": "๐Ÿ˜",
107
- "admiration": "๐Ÿคฉ",
108
- "amusement": "๐Ÿ˜„",
109
- "annoyance": "๐Ÿ˜ค",
110
- "approval": "๐Ÿ‘",
111
- "caring": "๐Ÿค—",
112
- "confusion": "๐Ÿ˜•",
113
- "curiosity": "๐Ÿค”",
114
- "desire": "๐Ÿ˜",
115
- "disappointment": "๐Ÿ˜ž",
116
- "disapproval": "๐Ÿ‘Ž",
117
- "disgust": "๐Ÿคข",
118
- "embarrassment": "๐Ÿ˜ณ",
119
- "excitement": "๐ŸŽ‰",
120
- "gratitude": "๐Ÿ™",
121
- "grief": "๐Ÿ˜ญ",
122
- "nervousness": "๏ฟฝ๏ฟฝ",
123
- "optimism": "๐ŸŒŸ",
124
- "pride": "๐Ÿฆ",
125
- "realization": "๐Ÿ’ก",
126
- "relief": "๐Ÿ˜Œ",
127
- "remorse": "๐Ÿ˜”",
128
- }
129
-
130
  if emotions.labels:
131
  emotion_parts = []
132
  for lbl, score in zip(emotions.labels[:5], emotions.scores[:5], strict=False):
133
- emoji = emotion_emoji.get(lbl.lower(), "โ€ข")
134
  emotion_parts.append(f"{emoji} **{lbl.title()}** ({score:.0%})")
135
  emotion_str = "\n".join(emotion_parts)
136
  else:
137
  emotion_str = "๐Ÿ˜ No strong emotions detected"
138
 
139
  # Format topic
140
- topic_str = f"**{topic.label}**\n\nConfidence: {topic.confidence:.0%}"
 
141
 
142
  return summary, emotion_str, topic_str
143
 
@@ -146,9 +220,85 @@ def analyze(text: str) -> tuple[str, str, str]:
146
  return f"Error: {e}", "", ""
147
 
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  def load_metrics() -> str:
150
  """Load evaluation metrics and format as markdown."""
151
- # Load evaluation report
152
  eval_metrics = {}
153
  if EVAL_REPORT_PATH.exists():
154
  try:
@@ -157,7 +307,6 @@ def load_metrics() -> str:
157
  except Exception:
158
  pass
159
 
160
- # Load training history
161
  train_metrics = {}
162
  if TRAINING_HISTORY_PATH.exists():
163
  try:
@@ -166,18 +315,17 @@ def load_metrics() -> str:
166
  except Exception:
167
  pass
168
 
169
- # Get final validation metrics
170
  val_final = train_metrics.get("val_epoch_3", {})
171
 
172
  md = """
173
  ## ๐Ÿ“ˆ Model Performance
174
 
175
- ### Training Results (3 Epochs)
176
 
177
- | Task | Metric | Final Score |
178
- |------|--------|-------------|
179
  | **Topic Classification** | Accuracy | **{topic_acc:.1%}** |
180
- | **Emotion Detection** | F1 (training) | {emo_f1:.1%} |
181
  | **Summarization** | ROUGE-like | {rouge:.1%} |
182
 
183
  ### Evaluation Results
@@ -188,13 +336,6 @@ def load_metrics() -> str:
188
  | Emotion F1 (macro) | {eval_emo:.1%} |
189
  | ROUGE-like | {eval_rouge:.1%} |
190
  | BLEU | {eval_bleu:.3f} |
191
-
192
- ---
193
-
194
- ### Topic Classification Details
195
-
196
- | Category | Precision | Recall | F1 |
197
- |----------|-----------|--------|-----|
198
  """.format(
199
  topic_acc=val_final.get("topic_accuracy", 0),
200
  emo_f1=val_final.get("emotion_f1", 0),
@@ -205,183 +346,303 @@ def load_metrics() -> str:
205
  eval_bleu=eval_metrics.get("summarization", {}).get("bleu", 0),
206
  )
207
 
208
- # Add per-class metrics
209
- topic_report = eval_metrics.get("topic", {}).get("classification_report", {})
210
- for cat, metrics in topic_report.items():
211
- if cat in ["macro avg", "weighted avg", "micro avg"]:
212
- continue
213
- if isinstance(metrics, dict):
214
- md += f"| {cat} | {metrics.get('precision', 0):.1%} | {metrics.get('recall', 0):.1%} | {metrics.get('f1-score', 0):.1%} |\n"
215
-
216
  return md
217
 
218
 
219
- def get_viz_path(filename: str) -> str | None:
220
- """Get visualization path if file exists."""
221
- path = OUTPUTS_DIR / filename
222
- return str(path) if path.exists() else None
223
-
224
-
225
  # --------------- Gradio Interface ---------------
226
 
227
  with gr.Blocks(
228
  title="LexiMind - Multi-Task NLP",
229
  theme=gr.themes.Soft(),
 
 
 
 
230
  ) as demo:
231
  gr.Markdown(
232
  """
233
  # ๐Ÿง  LexiMind
234
  ### Multi-Task Transformer for Document Analysis
235
 
236
- A custom encoder-decoder Transformer trained on **summarization**, **emotion detection** (28 classes),
237
- and **topic classification** (10 categories). Built from scratch with PyTorch.
 
 
238
 
239
- > โš ๏ธ **Note**: Summarization is experimental - the model works best on news-style articles.
240
  """
241
  )
242
 
243
- # --------------- Try It Tab ---------------
244
- with gr.Tab("๐Ÿš€ Try It"):
 
 
 
 
 
 
 
 
245
  with gr.Row():
246
- with gr.Column(scale=3):
247
- text_input = gr.Textbox(
248
- label="๐Ÿ“ Input Text",
249
- lines=6,
250
- placeholder="Enter or paste text to analyze (works best with news articles)...",
251
- value=SAMPLE_TEXTS[0],
252
  )
253
- analyze_btn = gr.Button(
254
- "๐Ÿ” Analyze",
255
- variant="primary",
256
- size="sm",
 
 
 
 
 
257
  )
258
-
259
- gr.Markdown("**Sample Texts** (click to use):")
 
 
 
 
 
 
 
 
 
260
  with gr.Row():
261
- sample1_btn = gr.Button("๐Ÿ“ฐ Markets", size="sm", variant="secondary")
262
- sample2_btn = gr.Button("๐Ÿ”ฌ Science", size="sm", variant="secondary")
263
- sample3_btn = gr.Button("๐Ÿ† Sports", size="sm", variant="secondary")
264
-
265
- sample1_btn.click(fn=lambda: SAMPLE_TEXTS[0], outputs=text_input)
266
- sample2_btn.click(fn=lambda: SAMPLE_TEXTS[1], outputs=text_input)
267
- sample3_btn.click(fn=lambda: SAMPLE_TEXTS[2], outputs=text_input)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  with gr.Column(scale=2):
270
- gr.Markdown("### Results")
271
- summary_out = gr.Textbox(
272
- label="๐Ÿ“ Summary",
273
- lines=3,
274
  interactive=False,
275
  )
 
276
  with gr.Row():
277
  with gr.Column():
278
- gr.Markdown("**๐Ÿ˜Š Emotions**")
279
- emotion_out = gr.Markdown(value="*Run analysis*")
 
 
 
280
  with gr.Column():
281
- gr.Markdown("**๐Ÿ“‚ Topic**")
282
- topic_out = gr.Markdown(value="*Run analysis*")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
- analyze_btn.click(
285
- fn=analyze,
286
- inputs=text_input,
287
- outputs=[summary_out, emotion_out, topic_out],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  )
289
 
290
- # --------------- Metrics Tab ---------------
291
  with gr.Tab("๐Ÿ“Š Metrics"):
292
  with gr.Row():
293
  with gr.Column(scale=2):
294
  gr.Markdown(load_metrics())
295
  with gr.Column(scale=1):
296
- confusion_path = get_viz_path("topic_confusion_matrix.png")
297
- if confusion_path:
298
- gr.Image(confusion_path, label="Confusion Matrix", show_label=True)
299
-
300
- # --------------- Visualizations Tab ---------------
301
- with gr.Tab("๐ŸŽจ Visualizations"):
302
- gr.Markdown("### Model Internals")
303
-
304
- with gr.Row():
305
- attn_path = get_viz_path("attention_visualization.png")
306
- if attn_path:
307
- gr.Image(attn_path, label="Self-Attention Pattern")
308
-
309
- pos_path = get_viz_path("positional_encoding_heatmap.png")
310
- if pos_path:
311
- gr.Image(pos_path, label="Positional Encodings")
312
 
313
- with gr.Row():
314
- multi_path = get_viz_path("multihead_attention_visualization.png")
315
- if multi_path:
316
- gr.Image(multi_path, label="Multi-Head Attention")
317
-
318
- single_path = get_viz_path("single_vs_multihead.png")
319
- if single_path:
320
- gr.Image(single_path, label="Single vs Multi-Head Comparison")
321
-
322
- # --------------- Architecture Tab ---------------
323
- with gr.Tab("๐Ÿ”ง Architecture"):
324
  gr.Markdown(
325
  """
326
- ### Model Architecture
327
-
328
- | Component | Configuration |
329
- |-----------|---------------|
330
- | **Base** | Custom Transformer (encoder-decoder) |
331
- | **Initialization** | FLAN-T5-base weights |
332
- | **Encoder** | 6 layers, 768 hidden dim, 12 heads |
333
- | **Decoder** | 6 layers with cross-attention |
334
- | **Activation** | Gated-GELU |
335
- | **Position** | Relative position bias |
336
-
337
- ### Training Configuration
338
 
339
- | Setting | Value |
340
- |---------|-------|
341
- | **Optimizer** | AdamW (lr=2e-5, wd=0.01) |
342
- | **Scheduler** | Cosine with 1000 warmup steps |
343
- | **Batch Size** | 14 ร— 3 accumulation = 42 effective |
344
- | **Precision** | TF32 (Ampere GPU) |
345
- | **Compilation** | torch.compile (inductor) |
346
 
347
- ### Datasets
348
 
349
- | Task | Dataset | Size |
350
- |------|---------|------|
351
- | **Summarization** | CNN/DailyMail + BookSum | ~110K |
352
- | **Emotion** | GoEmotions | ~43K (28 labels) |
353
- | **Topic** | Yahoo Answers | ~200K (10 classes) |
354
- """
355
- )
356
-
357
- # --------------- About Tab ---------------
358
- with gr.Tab("โ„น๏ธ About"):
359
- gr.Markdown(
360
- """
361
- ### About LexiMind
362
 
363
- LexiMind is a **portfolio project** demonstrating end-to-end machine learning engineering:
364
 
365
- โœ… Custom Transformer implementation from scratch
366
- โœ… Multi-task learning with shared encoder
367
- โœ… Production-ready inference pipeline
368
- โœ… Comprehensive evaluation and visualization
369
- โœ… CI/CD with GitHub Actions
 
370
 
371
- ### Known Limitations
372
 
373
- - **Summarization** quality is limited (needs more training epochs)
374
- - **Emotion detection** has low F1 due to class imbalance in GoEmotions
375
- - Best results on **news-style text** (training domain)
376
 
377
- ### Links
378
 
379
- - ๐Ÿ”— [GitHub Repository](https://github.com/OliverPerrin/LexiMind)
380
- - ๐Ÿค— [Model on HuggingFace](https://huggingface.co/OliverPerrin/LexiMind-Model)
 
381
 
382
  ---
383
 
384
- **Built by Oliver Perrin** | December 2025
385
  """
386
  )
387
 
@@ -389,5 +650,12 @@ with gr.Blocks(
389
  # --------------- Entry Point ---------------
390
 
391
  if __name__ == "__main__":
392
- get_pipeline() # Pre-load to fail fast if checkpoint missing
 
 
 
 
 
 
 
393
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
  """
2
  Gradio demo for LexiMind multi-task NLP model.
3
 
4
+ Redesigned to showcase the model's capabilities on training data:
5
+ - Browse classic literature and news articles
6
+ - Filter by topic and emotion
7
+ - View real-time summaries and classifications
8
+ - Compare model outputs across different texts
9
 
10
  Author: Oliver Perrin
11
+ Date: 2025-12-05, Updated: 2026-01-12
12
  """
13
 
14
  from __future__ import annotations
15
 
16
  import json
17
+ import random
18
  import sys
19
  from pathlib import Path
20
+ from typing import Any
21
 
22
  import gradio as gr
23
 
 
40
  # --------------- Constants ---------------
41
 
42
  OUTPUTS_DIR = PROJECT_ROOT / "outputs"
43
+ DATA_DIR = PROJECT_ROOT / "data" / "processed"
44
+ BOOKS_DIR = DATA_DIR / "books"
45
+ SUMMARIZATION_DIR = DATA_DIR / "summarization"
46
+
47
  EVAL_REPORT_PATH = OUTPUTS_DIR / "evaluation_report.json"
48
  TRAINING_HISTORY_PATH = OUTPUTS_DIR / "training_history.json"
49
 
50
+ # Emotion display with emojis
51
+ EMOTION_EMOJI = {
52
+ "joy": "๐Ÿ˜Š", "love": "โค๏ธ", "anger": "๐Ÿ˜ ", "fear": "๐Ÿ˜จ",
53
+ "sadness": "๐Ÿ˜ข", "surprise": "๐Ÿ˜ฒ", "neutral": "๐Ÿ˜",
54
+ "admiration": "๐Ÿคฉ", "amusement": "๐Ÿ˜„", "annoyance": "๐Ÿ˜ค",
55
+ "approval": "๐Ÿ‘", "caring": "๐Ÿค—", "confusion": "๐Ÿ˜•",
56
+ "curiosity": "๐Ÿค”", "desire": "๐Ÿ˜", "disappointment": "๐Ÿ˜ž",
57
+ "disapproval": "๐Ÿ‘Ž", "disgust": "๐Ÿคข", "embarrassment": "๐Ÿ˜ณ",
58
+ "excitement": "๐ŸŽ‰", "gratitude": "๐Ÿ™", "grief": "๐Ÿ˜ญ",
59
+ "nervousness": "๐Ÿ˜ฐ", "optimism": "๐ŸŒŸ", "pride": "๐Ÿฆ",
60
+ "realization": "๐Ÿ’ก", "relief": "๐Ÿ˜Œ", "remorse": "๐Ÿ˜”",
61
+ }
62
+
63
+ # Topic display with emojis
64
+ TOPIC_EMOJI = {
65
+ "World": "๐ŸŒ", "Sports": "๐Ÿ†", "Business": "๐Ÿ’ผ",
66
+ "Sci/Tech": "๐Ÿ”ฌ", "Science & Mathematics": "๐Ÿ”ฌ",
67
+ "Education & Reference": "๐Ÿ“š", "Entertainment & Music": "๐ŸŽฌ",
68
+ "Health": "๐Ÿฅ", "Family & Relationships": "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง",
69
+ "Society & Culture": "๐Ÿ›๏ธ", "Politics & Government": "๐Ÿ—ณ๏ธ",
70
+ "Computers & Internet": "๐Ÿ’ป",
71
+ }
72
+
73
+ # --------------- Data Loading ---------------
74
+
75
+
76
+ def load_books_data() -> list[dict[str, Any]]:
77
+ """Load book paragraphs from JSONL files."""
78
+ books = []
79
+ library_path = BOOKS_DIR / "library.json"
80
+
81
+ if library_path.exists():
82
+ with open(library_path) as f:
83
+ library = json.load(f)
84
+
85
+ for book_info in library.get("books", []):
86
+ title = book_info["title"]
87
+ jsonl_name = book_info["filename"].replace(".txt", ".jsonl")
88
+ jsonl_path = BOOKS_DIR / jsonl_name
89
+
90
+ if jsonl_path.exists():
91
+ paragraphs = []
92
+ with open(jsonl_path) as f:
93
+ for line in f:
94
+ if line.strip():
95
+ para = json.loads(line)
96
+ # Only include paragraphs with substantial content
97
+ if para.get("token_count", 0) > 50:
98
+ paragraphs.append(para)
99
+
100
+ if paragraphs:
101
+ books.append({
102
+ "title": title,
103
+ "paragraphs": paragraphs[:20], # Limit to first 20 substantial paragraphs
104
+ "word_count": book_info.get("word_count", 0),
105
+ })
106
+
107
+ return books
108
+
109
+
110
+ def load_news_data(split: str = "validation", max_items: int = 100) -> list[dict[str, Any]]:
111
+ """Load news articles from summarization dataset."""
112
+ articles = []
113
+ data_path = SUMMARIZATION_DIR / f"{split}.jsonl"
114
+
115
+ if data_path.exists():
116
+ with open(data_path) as f:
117
+ for i, line in enumerate(f):
118
+ if i >= max_items:
119
+ break
120
+ if line.strip():
121
+ article = json.loads(line)
122
+ # Only include articles with reasonable length
123
+ source = article.get("source", "")
124
+ if len(source) > 200:
125
+ articles.append({
126
+ "text": source,
127
+ "reference_summary": article.get("summary", ""),
128
+ "id": i,
129
+ })
130
+
131
+ return articles
132
+
133
+
134
+ # Cache the loaded data
135
+ _books_cache: list[dict] | None = None
136
+ _news_cache: list[dict] | None = None
137
+
138
+
139
+ def get_books() -> list[dict]:
140
+ global _books_cache
141
+ if _books_cache is None:
142
+ _books_cache = load_books_data()
143
+ return _books_cache
144
+
145
+
146
+ def get_news() -> list[dict]:
147
+ global _news_cache
148
+ if _news_cache is None:
149
+ _news_cache = load_news_data()
150
+ return _news_cache
151
+
152
 
153
  # --------------- Pipeline Management ---------------
154
 
 
180
  return _pipeline
181
 
182
 
183
+ # --------------- Core Analysis Functions ---------------
184
 
185
 
186
+ def analyze_text(text: str) -> tuple[str, str, str]:
187
  """Run all three tasks and return formatted results."""
188
  if not text or not text.strip():
189
+ return "Please enter or select text to analyze.", "", ""
190
 
191
  try:
192
  pipe = get_pipeline()
193
 
194
  # Run tasks
195
+ summary = pipe.summarize([text], max_length=150)[0].strip()
196
  if not summary:
197
  summary = "(Unable to generate summary)"
198
 
199
+ emotions = pipe.predict_emotions([text], threshold=0.3)[0]
200
  topic = pipe.predict_topics([text])[0]
201
 
202
+ # Format emotions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  if emotions.labels:
204
  emotion_parts = []
205
  for lbl, score in zip(emotions.labels[:5], emotions.scores[:5], strict=False):
206
+ emoji = EMOTION_EMOJI.get(lbl.lower(), "โ€ข")
207
  emotion_parts.append(f"{emoji} **{lbl.title()}** ({score:.0%})")
208
  emotion_str = "\n".join(emotion_parts)
209
  else:
210
  emotion_str = "๐Ÿ˜ No strong emotions detected"
211
 
212
  # Format topic
213
+ topic_emoji = TOPIC_EMOJI.get(topic.label, "๐Ÿ“„")
214
+ topic_str = f"{topic_emoji} **{topic.label}**\n\nConfidence: {topic.confidence:.0%}"
215
 
216
  return summary, emotion_str, topic_str
217
 
 
220
  return f"Error: {e}", "", ""
221
 
222
 
223
+ # --------------- Book Browser Functions ---------------
224
+
225
+
226
+ def get_book_titles() -> list[str]:
227
+ """Get list of available book titles."""
228
+ books = get_books()
229
+ return [b["title"] for b in books]
230
+
231
+
232
+ def get_book_excerpt(title: str, paragraph_idx: int = 0) -> str:
233
+ """Get a specific paragraph from a book."""
234
+ books = get_books()
235
+ for book in books:
236
+ if book["title"] == title:
237
+ paragraphs = book["paragraphs"]
238
+ if 0 <= paragraph_idx < len(paragraphs):
239
+ text = paragraphs[paragraph_idx].get("text", "")
240
+ return str(text) if text else ""
241
+ return ""
242
+
243
+
244
+ def get_book_info(title: str) -> str:
245
+ """Get book metadata."""
246
+ books = get_books()
247
+ for book in books:
248
+ if book["title"] == title:
249
+ num_paras = len(book["paragraphs"])
250
+ word_count = book["word_count"]
251
+ return f"**{title}**\n\n๐Ÿ“– {word_count:,} words | {num_paras} excerpts available"
252
+ return ""
253
+
254
+
255
+ def on_book_select(title: str) -> tuple[str, str, int]:
256
+ """Handle book selection - return first excerpt and info."""
257
+ info = get_book_info(title)
258
+ excerpt = get_book_excerpt(title, 0)
259
+ return info, excerpt, 0
260
+
261
+
262
+ def on_paragraph_change(title: str, idx: int) -> str:
263
+ """Handle paragraph slider change."""
264
+ return get_book_excerpt(title, int(idx))
265
+
266
+
267
+ def get_max_paragraphs(title: str) -> int:
268
+ """Get the number of paragraphs for a book."""
269
+ books = get_books()
270
+ for book in books:
271
+ if book["title"] == title:
272
+ return len(book["paragraphs"]) - 1
273
+ return 0
274
+
275
+
276
+ # --------------- News Browser Functions ---------------
277
+
278
+
279
+ def get_random_news() -> tuple[str, str]:
280
+ """Get a random news article and its reference summary."""
281
+ news = get_news()
282
+ if news:
283
+ article = random.choice(news)
284
+ return article["text"], article.get("reference_summary", "")
285
+ return "", ""
286
+
287
+
288
+ def get_news_by_index(idx: int) -> tuple[str, str]:
289
+ """Get news article by index."""
290
+ news = get_news()
291
+ if 0 <= idx < len(news):
292
+ article = news[idx]
293
+ return article["text"], article.get("reference_summary", "")
294
+ return "", ""
295
+
296
+
297
+ # --------------- Metrics Loading ---------------
298
+
299
+
300
  def load_metrics() -> str:
301
  """Load evaluation metrics and format as markdown."""
 
302
  eval_metrics = {}
303
  if EVAL_REPORT_PATH.exists():
304
  try:
 
307
  except Exception:
308
  pass
309
 
 
310
  train_metrics = {}
311
  if TRAINING_HISTORY_PATH.exists():
312
  try:
 
315
  except Exception:
316
  pass
317
 
 
318
  val_final = train_metrics.get("val_epoch_3", {})
319
 
320
  md = """
321
  ## ๐Ÿ“ˆ Model Performance
322
 
323
+ ### Training Results
324
 
325
+ | Task | Metric | Score |
326
+ |------|--------|-------|
327
  | **Topic Classification** | Accuracy | **{topic_acc:.1%}** |
328
+ | **Emotion Detection** | F1 | {emo_f1:.1%} |
329
  | **Summarization** | ROUGE-like | {rouge:.1%} |
330
 
331
  ### Evaluation Results
 
336
  | Emotion F1 (macro) | {eval_emo:.1%} |
337
  | ROUGE-like | {eval_rouge:.1%} |
338
  | BLEU | {eval_bleu:.3f} |
 
 
 
 
 
 
 
339
  """.format(
340
  topic_acc=val_final.get("topic_accuracy", 0),
341
  emo_f1=val_final.get("emotion_f1", 0),
 
346
  eval_bleu=eval_metrics.get("summarization", {}).get("bleu", 0),
347
  )
348
 
 
 
 
 
 
 
 
 
349
  return md
350
 
351
 
 
 
 
 
 
 
352
  # --------------- Gradio Interface ---------------
353
 
354
  with gr.Blocks(
355
  title="LexiMind - Multi-Task NLP",
356
  theme=gr.themes.Soft(),
357
+ css="""
358
+ .book-card { padding: 10px; border-radius: 8px; background: #f0f4f8; }
359
+ .results-panel { min-height: 200px; }
360
+ """
361
  ) as demo:
362
  gr.Markdown(
363
  """
364
  # ๐Ÿง  LexiMind
365
  ### Multi-Task Transformer for Document Analysis
366
 
367
+ Explore classic literature and news articles with AI-powered analysis:
368
+ - ๐Ÿ“ **Summarization** - Generate concise summaries
369
+ - ๐Ÿ˜Š **Emotion Detection** - Identify emotional tones
370
+ - ๐Ÿ“‚ **Topic Classification** - Categorize by subject
371
 
372
+ > Built with a custom Transformer initialized from FLAN-T5 weights.
373
  """
374
  )
375
 
376
+ # ===================== TAB 1: EXPLORE BOOKS =====================
377
+ with gr.Tab("๐Ÿ“š Explore Books"):
378
+ gr.Markdown(
379
+ """
380
+ ### Classic Literature Collection
381
+ Browse excerpts from classic novels and see how LexiMind analyzes them.
382
+ Select a book, navigate through excerpts, and click **Analyze** to run the model.
383
+ """
384
+ )
385
+
386
  with gr.Row():
387
+ with gr.Column(scale=1):
388
+ book_dropdown = gr.Dropdown(
389
+ choices=get_book_titles(),
390
+ label="๐Ÿ“– Select a Book",
391
+ value=get_book_titles()[0] if get_book_titles() else None,
 
392
  )
393
+ book_info = gr.Markdown(elem_classes=["book-card"])
394
+
395
+ para_slider = gr.Slider(
396
+ minimum=0,
397
+ maximum=19,
398
+ step=1,
399
+ value=0,
400
+ label="๐Ÿ“„ Excerpt Number",
401
+ info="Navigate through different parts of the book"
402
  )
403
+
404
+ analyze_book_btn = gr.Button("๐Ÿ” Analyze This Excerpt", variant="primary")
405
+
406
+ with gr.Column(scale=2):
407
+ book_excerpt = gr.Textbox(
408
+ label="๐Ÿ“œ Book Excerpt",
409
+ lines=10,
410
+ max_lines=15,
411
+ interactive=False,
412
+ )
413
+
414
  with gr.Row():
415
+ with gr.Column():
416
+ book_summary = gr.Textbox(
417
+ label="๐Ÿ“ Generated Summary",
418
+ lines=4,
419
+ interactive=False,
420
+ )
421
+ with gr.Column():
422
+ with gr.Row():
423
+ book_emotions = gr.Markdown(
424
+ label="๐Ÿ˜Š Emotions",
425
+ value="*Click Analyze*",
426
+ )
427
+ book_topic = gr.Markdown(
428
+ label="๐Ÿ“‚ Topic",
429
+ value="*Click Analyze*",
430
+ )
431
+
432
+ # Book event handlers
433
+ book_dropdown.change(
434
+ fn=on_book_select,
435
+ inputs=[book_dropdown],
436
+ outputs=[book_info, book_excerpt, para_slider],
437
+ )
438
+
439
+ para_slider.change(
440
+ fn=on_paragraph_change,
441
+ inputs=[book_dropdown, para_slider],
442
+ outputs=[book_excerpt],
443
+ )
444
+
445
+ analyze_book_btn.click(
446
+ fn=analyze_text,
447
+ inputs=[book_excerpt],
448
+ outputs=[book_summary, book_emotions, book_topic],
449
+ )
450
+
451
+ # Initialize with first book
452
+ demo.load(
453
+ fn=on_book_select,
454
+ inputs=[book_dropdown],
455
+ outputs=[book_info, book_excerpt, para_slider],
456
+ )
457
 
458
+ # ===================== TAB 2: EXPLORE NEWS =====================
459
+ with gr.Tab("๐Ÿ“ฐ Explore News"):
460
+ gr.Markdown(
461
+ """
462
+ ### CNN/DailyMail News Articles
463
+ Explore news articles from the training dataset. Compare the model's
464
+ generated summary with the original human-written summary.
465
+ """
466
+ )
467
+
468
+ with gr.Row():
469
+ with gr.Column(scale=1):
470
+ news_slider = gr.Slider(
471
+ minimum=0,
472
+ maximum=99,
473
+ step=1,
474
+ value=0,
475
+ label="๐Ÿ“ฐ Article Number",
476
+ )
477
+ random_news_btn = gr.Button("๐ŸŽฒ Random Article", variant="secondary")
478
+ analyze_news_btn = gr.Button("๐Ÿ” Analyze Article", variant="primary")
479
+
480
+ gr.Markdown("### Reference Summary")
481
+ gr.Markdown("*Original human-written summary from the dataset:*")
482
+ reference_summary = gr.Textbox(
483
+ label="",
484
+ lines=4,
485
+ interactive=False,
486
+ show_label=False,
487
+ )
488
+
489
  with gr.Column(scale=2):
490
+ news_text = gr.Textbox(
491
+ label="๐Ÿ“ฐ News Article",
492
+ lines=12,
493
+ max_lines=15,
494
  interactive=False,
495
  )
496
+
497
  with gr.Row():
498
  with gr.Column():
499
+ news_summary = gr.Textbox(
500
+ label="๐Ÿ“ LexiMind Summary",
501
+ lines=4,
502
+ interactive=False,
503
+ )
504
  with gr.Column():
505
+ with gr.Row():
506
+ news_emotions = gr.Markdown(
507
+ label="๐Ÿ˜Š Emotions",
508
+ value="*Click Analyze*",
509
+ )
510
+ news_topic = gr.Markdown(
511
+ label="๐Ÿ“‚ Topic",
512
+ value="*Click Analyze*",
513
+ )
514
+
515
+ # News event handlers
516
+ news_slider.change(
517
+ fn=get_news_by_index,
518
+ inputs=[news_slider],
519
+ outputs=[news_text, reference_summary],
520
+ )
521
+
522
+ random_news_btn.click(
523
+ fn=get_random_news,
524
+ outputs=[news_text, reference_summary],
525
+ )
526
+
527
+ analyze_news_btn.click(
528
+ fn=analyze_text,
529
+ inputs=[news_text],
530
+ outputs=[news_summary, news_emotions, news_topic],
531
+ )
532
+
533
+ # Initialize with first article
534
+ demo.load(
535
+ fn=lambda: get_news_by_index(0),
536
+ outputs=[news_text, reference_summary],
537
+ )
538
 
539
+ # ===================== TAB 3: FREE TEXT =====================
540
+ with gr.Tab("โœ๏ธ Free Text"):
541
+ gr.Markdown(
542
+ """
543
+ ### Try Your Own Text
544
+ Enter any text to analyze. Note that the model performs best on
545
+ **news-style articles** and **literary prose** similar to the training data.
546
+ """
547
+ )
548
+
549
+ with gr.Row():
550
+ with gr.Column(scale=3):
551
+ free_text_input = gr.Textbox(
552
+ label="๐Ÿ“ Enter Text",
553
+ lines=8,
554
+ placeholder="Paste or type your text here...\n\nThe model works best with news articles or literary passages.",
555
+ )
556
+
557
+ with gr.Row():
558
+ analyze_free_btn = gr.Button("๐Ÿ” Analyze", variant="primary")
559
+ clear_btn = gr.Button("๐Ÿ—‘๏ธ Clear", variant="secondary")
560
+
561
+ gr.Markdown("**Sample texts:**")
562
+ with gr.Row():
563
+ sample1 = gr.Button("๐Ÿ“ˆ Business News", size="sm")
564
+ sample2 = gr.Button("๐Ÿ”ฌ Science News", size="sm")
565
+ sample3 = gr.Button("๐Ÿ† Sports News", size="sm")
566
+
567
+ with gr.Column(scale=2):
568
+ free_summary = gr.Textbox(
569
+ label="๐Ÿ“ Summary",
570
+ lines=4,
571
+ interactive=False,
572
+ )
573
+ with gr.Row():
574
+ free_emotions = gr.Markdown(value="*Enter text and click Analyze*")
575
+ free_topic = gr.Markdown(value="")
576
+
577
+ # Sample texts
578
+ SAMPLES = {
579
+ "business": "Global markets tumbled today as investors reacted to rising inflation concerns. The Federal Reserve hinted at potential interest rate hikes, sending shockwaves through technology and banking sectors. Analysts predict continued volatility as economic uncertainty persists. Major indices fell by over 2%, with tech stocks leading the decline.",
580
+ "science": "Scientists at MIT have developed a breakthrough quantum computing chip that operates at room temperature. This advancement could revolutionize drug discovery, cryptography, and artificial intelligence. The research team published their findings in Nature, demonstrating stable qubit operations for over 100 microseconds.",
581
+ "sports": "The championship game ended in dramatic fashion as the underdog team scored in the final seconds to secure victory. Fans rushed the field in celebration, marking the team's first title in 25 years. The winning goal came from a rookie player who had only joined the team this season.",
582
+ }
583
+
584
+ sample1.click(fn=lambda: SAMPLES["business"], outputs=free_text_input)
585
+ sample2.click(fn=lambda: SAMPLES["science"], outputs=free_text_input)
586
+ sample3.click(fn=lambda: SAMPLES["sports"], outputs=free_text_input)
587
+ clear_btn.click(fn=lambda: ("", "", "", ""), outputs=[free_text_input, free_summary, free_emotions, free_topic])
588
+
589
+ analyze_free_btn.click(
590
+ fn=analyze_text,
591
+ inputs=[free_text_input],
592
+ outputs=[free_summary, free_emotions, free_topic],
593
  )
594
 
595
+ # ===================== TAB 4: METRICS =====================
596
  with gr.Tab("๐Ÿ“Š Metrics"):
597
  with gr.Row():
598
  with gr.Column(scale=2):
599
  gr.Markdown(load_metrics())
600
  with gr.Column(scale=1):
601
+ confusion_path = OUTPUTS_DIR / "topic_confusion_matrix.png"
602
+ if confusion_path.exists():
603
+ gr.Image(str(confusion_path), label="Topic Confusion Matrix")
 
 
 
 
 
 
 
 
 
 
 
 
 
604
 
605
+ # ===================== TAB 5: ABOUT =====================
606
+ with gr.Tab("โ„น๏ธ About"):
 
 
 
 
 
 
 
 
 
607
  gr.Markdown(
608
  """
609
+ ### About LexiMind
 
 
 
 
 
 
 
 
 
 
 
610
 
611
+ LexiMind is a **multi-task NLP system** built from scratch with PyTorch,
612
+ demonstrating end-to-end machine learning engineering.
 
 
 
 
 
613
 
614
+ #### ๐Ÿ—๏ธ Architecture
615
 
616
+ - **Custom Transformer** encoder-decoder (12 layers each)
617
+ - **Pre-LN with RMSNorm** for training stability
618
+ - **T5 Relative Position Bias** for sequence modeling
619
+ - **FLAN-T5-base** weight initialization
620
+ - **Task-specific heads**: LM head (summarization), Classification heads (emotion, topic)
 
 
 
 
 
 
 
 
621
 
622
+ #### ๐Ÿ“š Training Data
623
 
624
+ | Task | Dataset | Description |
625
+ |------|---------|-------------|
626
+ | Summarization | CNN/DailyMail | ~100K news articles with summaries |
627
+ | Emotion | GoEmotions | Multi-label emotion classification |
628
+ | Topic | AG News | 4-class news categorization |
629
+ | Books | Project Gutenberg | 8 classic novels for evaluation |
630
 
631
+ #### โš ๏ธ Known Limitations
632
 
633
+ - **Domain-specific**: Best results on news articles and literary text
634
+ - **Summarization quality**: Limited by model size and training data
635
+ - **Generalization**: May struggle with very different text styles
636
 
637
+ #### ๐Ÿ”— Links
638
 
639
+ - [GitHub Repository](https://github.com/OliverPerrin/LexiMind)
640
+ - [Model on HuggingFace](https://huggingface.co/OliverPerrin/LexiMind-Model)
641
+ - [HuggingFace Space](https://huggingface.co/spaces/OliverPerrin/LexiMind)
642
 
643
  ---
644
 
645
+ **Built by Oliver Perrin** | Appalachian State University | 2025-2026
646
  """
647
  )
648
 
 
650
  # --------------- Entry Point ---------------
651
 
652
  if __name__ == "__main__":
653
+ # Pre-load pipeline and data
654
+ logger.info("Loading inference pipeline...")
655
+ get_pipeline()
656
+ logger.info("Loading book data...")
657
+ get_books()
658
+ logger.info("Loading news data...")
659
+ get_news()
660
+ logger.info("Starting Gradio server...")
661
  demo.launch(server_name="0.0.0.0", server_port=7860)
src/training/safe_compile.py CHANGED
@@ -2,7 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
5
- from typing import Any
6
 
7
  import torch
8
 
@@ -25,7 +25,10 @@ def compile_model_safe(
25
  Parameters mirror `torch.compile` but default to conservative settings.
26
  """
27
 
28
- return torch.compile(model, backend="inductor", mode=mode, dynamic=dynamic)
 
 
 
29
 
30
 
31
  def apply_safe_config() -> None:
 
2
 
3
  from __future__ import annotations
4
 
5
+ from typing import Any, cast
6
 
7
  import torch
8
 
 
25
  Parameters mirror `torch.compile` but default to conservative settings.
26
  """
27
 
28
+ return cast(
29
+ torch.nn.Module,
30
+ torch.compile(model, backend="inductor", mode=mode, dynamic=dynamic),
31
+ )
32
 
33
 
34
  def apply_safe_config() -> None: