entropy25 commited on
Commit
625477d
·
verified ·
1 Parent(s): f47b004

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +415 -466
app.py CHANGED
@@ -14,186 +14,364 @@ import io
14
  import tempfile
15
  import os
16
  from datetime import datetime
 
 
 
 
 
 
 
17
 
18
  # Configuration
19
- MAX_HISTORY_SIZE = 1000
20
- BATCH_SIZE_LIMIT = 50
21
- THEMES = {
22
- 'default': {'pos': '#4ecdc4', 'neg': '#ff6b6b'},
23
- 'ocean': {'pos': '#0077be', 'neg': '#ff6b35'},
24
- 'forest': {'pos': '#228b22', 'neg': '#dc143c'},
25
- 'sunset': {'pos': '#ff8c00', 'neg': '#8b0000'}
26
- }
 
 
 
 
 
 
 
27
 
28
- # Load model
29
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
30
- tokenizer = BertTokenizer.from_pretrained("entropy25/sentimentanalysis")
31
- model = BertForSequenceClassification.from_pretrained("entropy25/sentimentanalysis")
32
- model.to(device)
33
 
34
- # Global storage with size limit
35
- history = []
 
36
 
37
- def manage_history_size():
38
- """Keep history size under limit"""
39
- global history
40
- if len(history) > MAX_HISTORY_SIZE:
41
- history = history[-MAX_HISTORY_SIZE:]
 
 
 
42
 
43
- def clean_text(text):
44
- """Simple text preprocessing"""
45
- text = re.sub(r'[^\w\s]', '', text.lower())
46
- words = text.split()
47
- stopwords = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'will', 'would', 'could', 'should', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them'}
48
- return [w for w in words if w not in stopwords and len(w) > 2]
 
 
 
 
49
 
50
- def analyze_text(text, theme='default'):
51
- """Core sentiment analysis"""
52
- if not text.strip():
53
- return "Please enter text", None, None, None
54
-
55
- inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
56
- with torch.no_grad():
57
- outputs = model(**inputs)
58
- probs = torch.nn.functional.softmax(outputs.logits, dim=-1).cpu().numpy()[0]
59
- pred = torch.argmax(outputs.logits, dim=-1).item()
60
- conf = probs.max()
61
- sentiment = "Positive" if pred == 1 else "Negative"
62
-
63
- # Store in history with timestamp
64
- history.append({
65
- 'text': text[:100],
66
- 'full_text': text,
67
- 'sentiment': sentiment,
68
- 'confidence': conf,
69
- 'pos_prob': probs[1],
70
- 'neg_prob': probs[0],
71
- 'timestamp': datetime.now().isoformat()
72
- })
73
-
74
- manage_history_size()
75
-
76
- result = f"Sentiment: {sentiment} (Confidence: {conf:.3f})"
77
-
78
- # Generate plots
79
- prob_plot = plot_probs(probs, theme)
80
- gauge_plot = plot_gauge(conf, sentiment, theme)
81
- cloud_plot = plot_wordcloud(text, sentiment, theme)
82
-
83
- return result, prob_plot, gauge_plot, cloud_plot
84
 
85
- def plot_probs(probs, theme='default'):
86
- """Probability bar chart"""
87
- fig, ax = plt.subplots(figsize=(8, 5))
88
- labels = ["Negative", "Positive"]
89
- colors = [THEMES[theme]['neg'], THEMES[theme]['pos']]
90
-
91
- bars = ax.bar(labels, probs, color=colors, alpha=0.8)
92
- ax.set_title("Sentiment Probabilities", fontweight='bold')
93
- ax.set_ylabel("Probability")
94
- ax.set_ylim(0, 1)
95
-
96
- for bar, prob in zip(bars, probs):
97
- ax.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.02,
98
- f'{prob:.3f}', ha='center', va='bottom', fontweight='bold')
99
-
100
- plt.tight_layout()
101
- return fig
102
 
103
- def plot_gauge(conf, sentiment, theme='default'):
104
- """Confidence gauge"""
105
- fig, ax = plt.subplots(figsize=(8, 6))
106
-
107
- theta = np.linspace(0, np.pi, 100)
108
- colors = plt.cm.RdYlGn(np.linspace(0.2, 0.8, 100))
109
-
110
- for i in range(len(theta)-1):
111
- ax.fill_between([theta[i], theta[i+1]], [0, 0], [0.8, 0.8],
112
- color=colors[i], alpha=0.7)
113
-
114
- pos = np.pi * (0.5 + (0.4 if sentiment == 'Positive' else -0.4) * conf)
115
- ax.plot([pos, pos], [0, 0.6], 'k-', linewidth=6)
116
- ax.plot(pos, 0.6, 'ko', markersize=10)
117
-
118
- ax.set_xlim(0, np.pi)
119
- ax.set_ylim(0, 1)
120
- ax.set_title(f'{sentiment} - Confidence: {conf:.3f}', fontweight='bold')
121
- ax.set_xticks([0, np.pi/2, np.pi])
122
- ax.set_xticklabels(['Negative', 'Neutral', 'Positive'])
123
- ax.set_yticks([])
124
- ax.axis('off')
125
-
126
- plt.tight_layout()
127
- return fig
128
 
129
- def plot_wordcloud(text, sentiment, theme='default'):
130
- """Word cloud visualization"""
131
- if len(text.split()) < 3:
132
- return None
133
-
134
- colormap = 'Greens' if sentiment == 'Positive' else 'Reds'
135
- wc = WordCloud(width=800, height=400, background_color='white',
136
- colormap=colormap, max_words=30).generate(text)
137
-
138
- fig, ax = plt.subplots(figsize=(10, 5))
139
- ax.imshow(wc, interpolation='bilinear')
140
- ax.axis('off')
141
- ax.set_title(f'{sentiment} Word Cloud', fontweight='bold')
142
-
143
- plt.tight_layout()
144
- return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
- def batch_analysis(reviews, progress=gr.Progress()):
147
- """Analyze multiple reviews with progress tracking"""
148
- if not reviews.strip():
149
- return None
150
-
151
- texts = [r.strip() for r in reviews.split('\n') if r.strip()]
152
- if len(texts) < 2:
153
- return None
154
-
155
- # Apply batch size limit
156
- if len(texts) > BATCH_SIZE_LIMIT:
157
- texts = texts[:BATCH_SIZE_LIMIT]
158
-
159
- results = []
 
 
 
 
 
 
 
160
 
161
- for i, text in enumerate(texts):
162
- progress((i + 1) / len(texts), f"Processing review {i + 1}/{len(texts)}")
163
-
164
- # Process in smaller GPU batches
165
- inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
166
- with torch.no_grad():
167
- outputs = model(**inputs)
168
- probs = torch.nn.functional.softmax(outputs.logits, dim=-1).cpu().numpy()[0]
169
- pred = torch.argmax(outputs.logits, dim=-1).item()
170
- sentiment = "Positive" if pred == 1 else "Negative"
171
- conf = probs.max()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
- results.append({
174
- 'text': text[:50] + '...' if len(text) > 50 else text,
175
- 'sentiment': sentiment,
176
- 'confidence': conf,
177
- 'pos_prob': probs[1]
178
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
  # Add to history
181
- history.append({
182
  'text': text[:100],
183
  'full_text': text,
184
- 'sentiment': sentiment,
185
- 'confidence': conf,
186
- 'pos_prob': probs[1],
187
- 'neg_prob': probs[0],
188
  'timestamp': datetime.now().isoformat()
189
- })
190
-
191
- manage_history_size()
192
-
193
- # Create visualization
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10))
195
 
196
- # Pie chart
197
  sent_counts = Counter([r['sentiment'] for r in results])
198
  colors = ['#4ecdc4', '#ff6b6b']
199
  ax1.pie(sent_counts.values(), labels=sent_counts.keys(),
@@ -207,7 +385,7 @@ def batch_analysis(reviews, progress=gr.Progress()):
207
  ax2.set_xlabel('Confidence')
208
  ax2.set_ylabel('Count')
209
 
210
- # Probability scatter
211
  indices = range(len(results))
212
  pos_probs = [r['pos_prob'] for r in results]
213
  ax3.scatter(indices, pos_probs,
@@ -218,7 +396,7 @@ def batch_analysis(reviews, progress=gr.Progress()):
218
  ax3.set_xlabel('Review Index')
219
  ax3.set_ylabel('Positive Probability')
220
 
221
- # Confidence vs Sentiment
222
  sent_binary = [1 if r['sentiment'] == 'Positive' else 0 for r in results]
223
  ax4.scatter(confs, sent_binary, alpha=0.7, s=100,
224
  c=['#4ecdc4' if s == 1 else '#ff6b6b' for s in sent_binary])
@@ -231,300 +409,93 @@ def batch_analysis(reviews, progress=gr.Progress()):
231
  plt.tight_layout()
232
  return fig
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  def process_uploaded_file(file):
235
- """Process uploaded CSV/TXT file for batch analysis"""
236
  if file is None:
237
  return ""
238
 
239
- content = file.read().decode('utf-8')
240
-
241
- # Handle CSV format
242
- if file.name.endswith('.csv'):
243
- lines = content.split('\n')
244
- # Assume text is in first column or look for 'review' column
245
- if ',' in content:
246
- reviews = []
247
- reader = csv.reader(lines)
248
- headers = next(reader, None)
249
- if headers and any('review' in h.lower() for h in headers):
250
- review_idx = next(i for i, h in enumerate(headers) if 'review' in h.lower())
251
- for row in reader:
252
- if len(row) > review_idx:
253
- reviews.append(row[review_idx])
254
- else:
255
- for row in reader:
256
- if row:
257
- reviews.append(row[0])
258
- return '\n'.join(reviews)
259
-
260
- # Handle plain text
261
- return content
262
-
263
- def export_history_csv():
264
- """Export history to CSV file"""
265
- if not history:
266
- return None, "No history to export"
267
-
268
- # Create temporary file
269
- temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv', encoding='utf-8')
270
- writer = csv.writer(temp_file)
271
- writer.writerow(['Timestamp', 'Text', 'Sentiment', 'Confidence', 'Positive_Prob', 'Negative_Prob'])
272
-
273
- for entry in history:
274
- writer.writerow([
275
- entry['timestamp'], entry['text'], entry['sentiment'],
276
- f"{entry['confidence']:.4f}", f"{entry['pos_prob']:.4f}", f"{entry['neg_prob']:.4f}"
277
- ])
278
-
279
- temp_file.close()
280
- return temp_file.name, f"Exported {len(history)} entries to CSV"
281
-
282
- def export_history_json():
283
- """Export history to JSON file"""
284
- if not history:
285
- return None, "No history to export"
286
-
287
- # Create temporary file
288
- temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json', encoding='utf-8')
289
- json.dump(history, temp_file, indent=2, ensure_ascii=False)
290
- temp_file.close()
291
-
292
- return temp_file.name, f"Exported {len(history)} entries to JSON"
293
-
294
- def keyword_heatmap():
295
- """Keyword sentiment heatmap"""
296
- if len(history) < 3:
297
- return None
298
-
299
- word_stats = defaultdict(list)
300
-
301
- for item in history:
302
- words = clean_text(item['full_text'])
303
- sentiment_score = item['pos_prob']
304
 
305
- for word in words:
306
- word_stats[word].append(sentiment_score)
307
-
308
- # Filter words with at least 2 occurrences
309
- filtered = {w: scores for w, scores in word_stats.items() if len(scores) >= 2}
310
-
311
- if len(filtered) < 5:
312
- return None
313
-
314
- # Get top 20 most frequent words
315
- top_words = sorted(filtered.items(), key=lambda x: len(x[1]), reverse=True)[:20]
316
-
317
- words = [item[0] for item in top_words]
318
- avg_sentiments = [np.mean(item[1]) for item in top_words]
319
- frequencies = [len(item[1]) for item in top_words]
320
-
321
- # Create heatmap data
322
- data = np.array([avg_sentiments, [f/max(frequencies) for f in frequencies]]).T
323
-
324
- fig, ax = plt.subplots(figsize=(12, 8))
325
-
326
- im = ax.imshow(data, cmap='RdYlGn', aspect='auto')
327
-
328
- ax.set_xticks([0, 1])
329
- ax.set_xticklabels(['Avg Sentiment', 'Frequency'])
330
- ax.set_yticks(range(len(words)))
331
- ax.set_yticklabels(words)
332
-
333
- # Add text annotations
334
- for i in range(len(words)):
335
- ax.text(0, i, f'{avg_sentiments[i]:.2f}', ha='center', va='center',
336
- color='black', fontweight='bold')
337
- ax.text(1, i, f'{frequencies[i]}', ha='center', va='center',
338
- color='black', fontweight='bold')
339
-
340
- ax.set_title('Keyword Sentiment Heatmap', fontweight='bold')
341
- plt.colorbar(im, ax=ax, label='Intensity')
342
-
343
- plt.tight_layout()
344
- return fig
345
 
346
- def cooccurrence_network():
347
- """Word co-occurrence network"""
348
- if len(history) < 3:
349
- return None
350
-
351
- all_words = []
352
- for item in history:
353
- words = clean_text(item['full_text'])
354
- if len(words) >= 3:
355
- all_words.extend(words)
356
-
357
- if len(all_words) < 10:
358
- return None
359
-
360
- word_freq = Counter(all_words)
361
- top_words = [word for word, freq in word_freq.most_common(15) if freq >= 2]
362
-
363
- if len(top_words) < 5:
364
- return None
365
-
366
- # Calculate co-occurrences
367
- cooccur = defaultdict(int)
368
-
369
- for item in history:
370
- words = [w for w in clean_text(item['full_text']) if w in top_words]
371
-
372
- for i, w1 in enumerate(words):
373
- for j, w2 in enumerate(words):
374
- if i != j and w1 != w2:
375
- pair = tuple(sorted([w1, w2]))
376
- cooccur[pair] += 1
377
-
378
- # Create network
379
- G = nx.Graph()
380
-
381
- for word in top_words:
382
- G.add_node(word, size=word_freq[word])
383
-
384
- for (w1, w2), weight in cooccur.items():
385
- if weight >= 2:
386
- G.add_edge(w1, w2, weight=weight)
387
-
388
- if len(G.edges()) == 0:
389
- return None
390
-
391
- # Plot network
392
- fig, ax = plt.subplots(figsize=(12, 10))
393
-
394
- pos = nx.spring_layout(G, k=3, iterations=50)
395
-
396
- node_sizes = [G.nodes[node]['size'] * 200 for node in G.nodes()]
397
- nx.draw_networkx_nodes(G, pos, node_size=node_sizes,
398
- node_color='lightblue', alpha=0.7, ax=ax)
399
-
400
- edges = G.edges()
401
- weights = [G[u][v]['weight'] for u, v in edges]
402
- nx.draw_networkx_edges(G, pos, width=[w*0.5 for w in weights],
403
- alpha=0.6, edge_color='gray', ax=ax)
404
-
405
- nx.draw_networkx_labels(G, pos, font_size=10, font_weight='bold', ax=ax)
406
-
407
- ax.set_title('Word Co-occurrence Network', fontweight='bold')
408
- ax.axis('off')
409
-
410
- plt.tight_layout()
411
- return fig
412
 
413
- def tfidf_analysis():
414
- """TF-IDF keyword analysis"""
415
- if len(history) < 4:
416
- return None
417
-
418
- pos_texts = []
419
- neg_texts = []
420
-
421
- for item in history:
422
- if item['sentiment'] == 'Positive':
423
- pos_texts.append(' '.join(clean_text(item['full_text'])))
424
- else:
425
- neg_texts.append(' '.join(clean_text(item['full_text'])))
426
-
427
- if len(pos_texts) < 2 or len(neg_texts) < 2:
428
- return None
429
-
430
- # Positive TF-IDF
431
- vectorizer_pos = TfidfVectorizer(max_features=50, ngram_range=(1, 2))
432
- pos_tfidf = vectorizer_pos.fit_transform(pos_texts)
433
- pos_features = vectorizer_pos.get_feature_names_out()
434
- pos_scores = pos_tfidf.sum(axis=0).A1
435
-
436
- # Negative TF-IDF
437
- vectorizer_neg = TfidfVectorizer(max_features=50, ngram_range=(1, 2))
438
- neg_tfidf = vectorizer_neg.fit_transform(neg_texts)
439
- neg_features = vectorizer_neg.get_feature_names_out()
440
- neg_scores = neg_tfidf.sum(axis=0).A1
441
-
442
- # Top 10 features
443
- pos_top_idx = np.argsort(pos_scores)[-10:][::-1]
444
- neg_top_idx = np.argsort(neg_scores)[-10:][::-1]
445
-
446
- pos_words = [pos_features[i] for i in pos_top_idx]
447
- pos_vals = [pos_scores[i] for i in pos_top_idx]
448
-
449
- neg_words = [neg_features[i] for i in neg_top_idx]
450
- neg_vals = [neg_scores[i] for i in neg_top_idx]
451
-
452
- # Plot
453
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8))
454
-
455
- # Positive
456
- bars1 = ax1.barh(pos_words, pos_vals, color='#4ecdc4', alpha=0.8)
457
- ax1.set_title('Positive Keywords (TF-IDF)', fontweight='bold')
458
- ax1.set_xlabel('TF-IDF Score')
459
-
460
- for bar, score in zip(bars1, pos_vals):
461
- ax1.text(bar.get_width() + 0.001, bar.get_y() + bar.get_height()/2,
462
- f'{score:.3f}', va='center', fontsize=9)
463
-
464
- # Negative
465
- bars2 = ax2.barh(neg_words, neg_vals, color='#ff6b6b', alpha=0.8)
466
- ax2.set_title('Negative Keywords (TF-IDF)', fontweight='bold')
467
- ax2.set_xlabel('TF-IDF Score')
468
-
469
- for bar, score in zip(bars2, neg_vals):
470
- ax2.text(bar.get_width() + 0.001, bar.get_y() + bar.get_height()/2,
471
- f'{score:.3f}', va='center', fontsize=9)
472
-
473
- plt.tight_layout()
474
- return fig
475
 
476
- def plot_history():
477
- """Analysis history visualization"""
478
- if len(history) < 2:
479
- return None, f"History contains {len(history)} entries. Need at least 2 for visualization."
480
-
481
- indices = list(range(len(history)))
482
- pos_probs = [item['pos_prob'] for item in history]
483
- confs = [item['confidence'] for item in history]
484
-
485
- fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
486
-
487
- colors = ['#4ecdc4' if p > 0.5 else '#ff6b6b' for p in pos_probs]
488
- ax1.scatter(indices, pos_probs, c=colors, alpha=0.7, s=100)
489
- ax1.plot(indices, pos_probs, alpha=0.5, linewidth=2)
490
- ax1.axhline(y=0.5, color='gray', linestyle='--', alpha=0.5)
491
- ax1.set_title('Sentiment History - Positive Probability')
492
- ax1.set_xlabel('Analysis Number')
493
- ax1.set_ylabel('Positive Probability')
494
- ax1.grid(True, alpha=0.3)
495
-
496
- ax2.bar(indices, confs, alpha=0.7, color='lightblue', edgecolor='navy')
497
- ax2.set_title('Confidence Scores Over Time')
498
- ax2.set_xlabel('Analysis Number')
499
- ax2.set_ylabel('Confidence Score')
500
- ax2.grid(True, alpha=0.3)
501
-
502
- plt.tight_layout()
503
- return fig, f"History contains {len(history)} analyses"
504
 
505
  def clear_history():
506
- """Clear analysis history"""
507
- global history
508
- count = len(history)
509
- history.clear()
510
  return f"Cleared {count} entries from history"
511
 
512
- def get_history_status():
513
- """Get current history status"""
514
- return f"History contains {len(history)} entries"
515
-
516
- # Enhanced example data
517
  EXAMPLE_REVIEWS = [
518
  ["The cinematography was stunning, but the plot felt predictable and the dialogue was weak."],
519
  ["A masterpiece of filmmaking! Amazing performances, brilliant direction, and unforgettable moments."],
520
  ["Boring movie with terrible acting, weak plot, and poor character development throughout."],
521
  ["Great special effects and action sequences, but the story was confusing and hard to follow."],
522
- ["Incredible ending that left me speechless! One of the best films I've ever seen."],
523
- ["The movie started strong but became repetitive and lost my interest halfway through."],
524
- ["Outstanding soundtrack and beautiful visuals, though the pacing was somewhat slow."],
525
- ["Disappointing sequel that failed to capture the magic of the original film."],
526
- ["Brilliant writing and exceptional acting make this a must-watch drama."],
527
- ["Generic blockbuster with predictable twists and forgettable characters."]
528
  ]
529
 
530
  # Gradio Interface
@@ -544,7 +515,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Movie Sentiment Analyzer") as demo
544
  with gr.Row():
545
  analyze_btn = gr.Button("Analyze", variant="primary", size="lg")
546
  theme_selector = gr.Dropdown(
547
- choices=list(THEMES.keys()),
548
  value="default",
549
  label="Color Theme"
550
  )
@@ -566,7 +537,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Movie Sentiment Analyzer") as demo
566
 
567
  with gr.Tab("Batch Analysis"):
568
  gr.Markdown("### Multiple Reviews Analysis")
569
- gr.Markdown(f"**Note:** Limited to {BATCH_SIZE_LIMIT} reviews per batch for optimal performance")
570
 
571
  with gr.Row():
572
  with gr.Column():
@@ -587,19 +558,6 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Movie Sentiment Analyzer") as demo
587
 
588
  batch_plot = gr.Plot(label="Batch Analysis Results")
589
 
590
- with gr.Tab("Advanced Analytics"):
591
- gr.Markdown("### Advanced Visualizations")
592
- gr.Markdown("**Requirements:** Minimum analysis history needed for each visualization")
593
-
594
- with gr.Row():
595
- heatmap_btn = gr.Button("Keyword Heatmap", variant="primary")
596
- network_btn = gr.Button("Word Network", variant="primary")
597
- tfidf_btn = gr.Button("TF-IDF Analysis", variant="primary")
598
-
599
- heatmap_plot = gr.Plot(label="Keyword Sentiment Heatmap")
600
- network_plot = gr.Plot(label="Word Co-occurrence Network")
601
- tfidf_plot = gr.Plot(label="TF-IDF Keywords Comparison")
602
-
603
  with gr.Tab("History & Export"):
604
  gr.Markdown("### Analysis History & Data Export")
605
 
@@ -615,7 +573,6 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Movie Sentiment Analyzer") as demo
615
  history_status = gr.Textbox(label="Status", interactive=False)
616
  history_plot = gr.Plot(label="Historical Analysis Trends")
617
 
618
- # File downloads
619
  csv_file_output = gr.File(label="Download CSV", visible=True)
620
  json_file_output = gr.File(label="Download JSON", visible=True)
621
 
@@ -638,10 +595,6 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Movie Sentiment Analyzer") as demo
638
  outputs=batch_plot
639
  )
640
 
641
- heatmap_btn.click(keyword_heatmap, outputs=heatmap_plot)
642
- network_btn.click(cooccurrence_network, outputs=network_plot)
643
- tfidf_btn.click(tfidf_analysis, outputs=tfidf_plot)
644
-
645
  refresh_btn.click(
646
  plot_history,
647
  outputs=[history_plot, history_status]
@@ -658,18 +611,14 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Movie Sentiment Analyzer") as demo
658
  )
659
 
660
  export_csv_btn.click(
661
- export_history_csv,
662
  outputs=[csv_file_output, history_status]
663
  )
664
 
665
  export_json_btn.click(
666
- export_history_json,
667
  outputs=[json_file_output, history_status]
668
  )
669
 
670
  if __name__ == "__main__":
671
- demo.launch(share=True)
672
-
673
-
674
-
675
-
 
14
  import tempfile
15
  import os
16
  from datetime import datetime
17
+ import logging
18
+ from functools import lru_cache
19
+ from dataclasses import dataclass
20
+ from typing import List, Dict, Optional, Tuple
21
+ import nltk
22
+ from nltk.corpus import stopwords
23
+ from nltk.tokenize import word_tokenize
24
 
25
  # Configuration
26
+ @dataclass
27
+ class Config:
28
+ MAX_HISTORY_SIZE: int = 1000
29
+ BATCH_SIZE_LIMIT: int = 50
30
+ MAX_TEXT_LENGTH: int = 512
31
+ MIN_WORD_LENGTH: int = 2
32
+ MIN_HISTORY_FOR_ANALYTICS: int = 3
33
+ CACHE_SIZE: int = 128
34
+
35
+ THEMES = {
36
+ 'default': {'pos': '#4ecdc4', 'neg': '#ff6b6b'},
37
+ 'ocean': {'pos': '#0077be', 'neg': '#ff6b35'},
38
+ 'forest': {'pos': '#228b22', 'neg': '#dc143c'},
39
+ 'sunset': {'pos': '#ff8c00', 'neg': '#8b0000'}
40
+ }
41
 
42
+ config = Config()
 
 
 
 
43
 
44
+ # Logging setup
45
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
46
+ logger = logging.getLogger(__name__)
47
 
48
+ # Download NLTK data (with error handling)
49
+ try:
50
+ nltk.download('stopwords', quiet=True)
51
+ nltk.download('punkt', quiet=True)
52
+ STOP_WORDS = set(stopwords.words('english'))
53
+ except:
54
+ logger.warning("NLTK not available, using fallback stopwords")
55
+ STOP_WORDS = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'will', 'would', 'could', 'should', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them'}
56
 
57
+ # Model initialization with error handling
58
+ try:
59
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
60
+ tokenizer = BertTokenizer.from_pretrained("entropy25/sentimentanalysis")
61
+ model = BertForSequenceClassification.from_pretrained("entropy25/sentimentanalysis")
62
+ model.to(device)
63
+ logger.info(f"Model loaded successfully on {device}")
64
+ except Exception as e:
65
+ logger.error(f"Model loading failed: {e}")
66
+ raise
67
 
68
+ class HistoryManager:
69
+ """Manages analysis history with size limits"""
70
+ def __init__(self):
71
+ self._history = []
72
+
73
+ def add_entry(self, entry: Dict):
74
+ """Add entry and manage size"""
75
+ self._history.append(entry)
76
+ if len(self._history) > config.MAX_HISTORY_SIZE:
77
+ self._history = self._history[-config.MAX_HISTORY_SIZE:]
78
+ logger.info(f"Added entry to history. Total: {len(self._history)}")
79
+
80
+ def get_history(self) -> List[Dict]:
81
+ return self._history.copy()
82
+
83
+ def clear(self) -> int:
84
+ count = len(self._history)
85
+ self._history.clear()
86
+ logger.info(f"Cleared {count} entries from history")
87
+ return count
88
+
89
+ def size(self) -> int:
90
+ return len(self._history)
 
 
 
 
 
 
 
 
 
 
 
91
 
92
+ history_manager = HistoryManager()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ class TextProcessor:
95
+ """Handles text preprocessing and analysis"""
96
+
97
+ @staticmethod
98
+ @lru_cache(maxsize=config.CACHE_SIZE)
99
+ def clean_text(text: str) -> Tuple[str, ...]:
100
+ """Clean and tokenize text with caching"""
101
+ try:
102
+ text = re.sub(r'[^\w\s]', '', text.lower())
103
+ words = text.split()
104
+ cleaned = [w for w in words if w not in STOP_WORDS and len(w) > config.MIN_WORD_LENGTH]
105
+ return tuple(cleaned) # Return tuple for hashability
106
+ except Exception as e:
107
+ logger.error(f"Text cleaning failed: {e}")
108
+ return tuple()
 
 
 
 
 
 
 
 
 
 
109
 
110
+ class SentimentAnalyzer:
111
+ """Core sentiment analysis functionality"""
112
+
113
+ @staticmethod
114
+ def analyze_single(text: str) -> Dict:
115
+ """Analyze single text with error handling"""
116
+ if not text.strip():
117
+ raise ValueError("Empty text provided")
118
+
119
+ try:
120
+ inputs = tokenizer(text, return_tensors="pt", padding=True,
121
+ truncation=True, max_length=config.MAX_TEXT_LENGTH).to(device)
122
+
123
+ with torch.no_grad():
124
+ outputs = model(**inputs)
125
+ probs = torch.nn.functional.softmax(outputs.logits, dim=-1).cpu().numpy()[0]
126
+ pred = torch.argmax(outputs.logits, dim=-1).item()
127
+
128
+ sentiment = "Positive" if pred == 1 else "Negative"
129
+ confidence = float(probs.max())
130
+
131
+ return {
132
+ 'sentiment': sentiment,
133
+ 'confidence': confidence,
134
+ 'pos_prob': float(probs[1]),
135
+ 'neg_prob': float(probs[0])
136
+ }
137
+ except Exception as e:
138
+ logger.error(f"Analysis failed: {e}")
139
+ raise
140
+
141
+ @staticmethod
142
+ def analyze_batch(texts: List[str], progress_callback=None) -> List[Dict]:
143
+ """Analyze multiple texts with true batch processing"""
144
+ if len(texts) > config.BATCH_SIZE_LIMIT:
145
+ texts = texts[:config.BATCH_SIZE_LIMIT]
146
+ logger.warning(f"Batch size limited to {config.BATCH_SIZE_LIMIT}")
147
+
148
+ results = []
149
+ try:
150
+ # Process in batches for memory efficiency
151
+ batch_size = 8
152
+ for i in range(0, len(texts), batch_size):
153
+ batch = texts[i:i+batch_size]
154
+
155
+ if progress_callback:
156
+ progress_callback((i + len(batch)) / len(texts),
157
+ f"Processing batch {i//batch_size + 1}")
158
+
159
+ # True batch processing
160
+ inputs = tokenizer(batch, return_tensors="pt", padding=True,
161
+ truncation=True, max_length=config.MAX_TEXT_LENGTH).to(device)
162
+
163
+ with torch.no_grad():
164
+ outputs = model(**inputs)
165
+ probs = torch.nn.functional.softmax(outputs.logits, dim=-1).cpu().numpy()
166
+ preds = torch.argmax(outputs.logits, dim=-1).cpu().numpy()
167
+
168
+ for j, (text, prob, pred) in enumerate(zip(batch, probs, preds)):
169
+ sentiment = "Positive" if pred == 1 else "Negative"
170
+ results.append({
171
+ 'text': text[:50] + '...' if len(text) > 50 else text,
172
+ 'full_text': text,
173
+ 'sentiment': sentiment,
174
+ 'confidence': float(prob.max()),
175
+ 'pos_prob': float(prob[1]),
176
+ 'neg_prob': float(prob[0])
177
+ })
178
+
179
+ except Exception as e:
180
+ logger.error(f"Batch analysis failed: {e}")
181
+ raise
182
+
183
+ return results
184
 
185
+ class Visualizer:
186
+ """Handles all visualization tasks"""
187
+
188
+ @staticmethod
189
+ def create_probability_plot(probs: np.ndarray, theme: str = 'default') -> plt.Figure:
190
+ """Create probability bar chart"""
191
+ fig, ax = plt.subplots(figsize=(8, 5))
192
+ labels = ["Negative", "Positive"]
193
+ colors = [config.THEMES[theme]['neg'], config.THEMES[theme]['pos']]
194
+
195
+ bars = ax.bar(labels, probs, color=colors, alpha=0.8)
196
+ ax.set_title("Sentiment Probabilities", fontweight='bold')
197
+ ax.set_ylabel("Probability")
198
+ ax.set_ylim(0, 1)
199
+
200
+ for bar, prob in zip(bars, probs):
201
+ ax.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.02,
202
+ f'{prob:.3f}', ha='center', va='bottom', fontweight='bold')
203
+
204
+ plt.tight_layout()
205
+ return fig
206
 
207
+ @staticmethod
208
+ def create_confidence_gauge(confidence: float, sentiment: str, theme: str = 'default') -> plt.Figure:
209
+ """Create confidence gauge visualization"""
210
+ fig, ax = plt.subplots(figsize=(8, 6))
211
+
212
+ theta = np.linspace(0, np.pi, 100)
213
+ colors = plt.cm.RdYlGn(np.linspace(0.2, 0.8, 100))
214
+
215
+ for i in range(len(theta)-1):
216
+ ax.fill_between([theta[i], theta[i+1]], [0, 0], [0.8, 0.8],
217
+ color=colors[i], alpha=0.7)
218
+
219
+ pos = np.pi * (0.5 + (0.4 if sentiment == 'Positive' else -0.4) * confidence)
220
+ ax.plot([pos, pos], [0, 0.6], 'k-', linewidth=6)
221
+ ax.plot(pos, 0.6, 'ko', markersize=10)
222
+
223
+ ax.set_xlim(0, np.pi)
224
+ ax.set_ylim(0, 1)
225
+ ax.set_title(f'{sentiment} - Confidence: {confidence:.3f}', fontweight='bold')
226
+ ax.set_xticks([0, np.pi/2, np.pi])
227
+ ax.set_xticklabels(['Negative', 'Neutral', 'Positive'])
228
+ ax.set_yticks([])
229
+ ax.axis('off')
230
+
231
+ plt.tight_layout()
232
+ return fig
233
+
234
+ @staticmethod
235
+ def create_wordcloud(text: str, sentiment: str, theme: str = 'default') -> Optional[plt.Figure]:
236
+ """Create word cloud visualization"""
237
+ if len(text.split()) < 3:
238
+ return None
239
+
240
+ try:
241
+ colormap = 'Greens' if sentiment == 'Positive' else 'Reds'
242
+ wc = WordCloud(width=800, height=400, background_color='white',
243
+ colormap=colormap, max_words=30).generate(text)
244
+
245
+ fig, ax = plt.subplots(figsize=(10, 5))
246
+ ax.imshow(wc, interpolation='bilinear')
247
+ ax.axis('off')
248
+ ax.set_title(f'{sentiment} Word Cloud', fontweight='bold')
249
+
250
+ plt.tight_layout()
251
+ return fig
252
+ except Exception as e:
253
+ logger.error(f"Word cloud generation failed: {e}")
254
+ return None
255
+
256
+ class DataExporter:
257
+ """Handles data export functionality"""
258
+
259
+ @staticmethod
260
+ def export_to_csv(history: List[Dict]) -> Tuple[Optional[str], str]:
261
+ """Export history to CSV"""
262
+ if not history:
263
+ return None, "No history to export"
264
+
265
+ try:
266
+ temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv', encoding='utf-8')
267
+ writer = csv.writer(temp_file)
268
+ writer.writerow(['Timestamp', 'Text', 'Sentiment', 'Confidence', 'Positive_Prob', 'Negative_Prob'])
269
 
270
+ for entry in history:
271
+ writer.writerow([
272
+ entry.get('timestamp', ''),
273
+ entry.get('text', ''),
274
+ entry.get('sentiment', ''),
275
+ f"{entry.get('confidence', 0):.4f}",
276
+ f"{entry.get('pos_prob', 0):.4f}",
277
+ f"{entry.get('neg_prob', 0):.4f}"
278
+ ])
279
+
280
+ temp_file.close()
281
+ logger.info(f"Exported {len(history)} entries to CSV")
282
+ return temp_file.name, f"Exported {len(history)} entries to CSV"
283
+ except Exception as e:
284
+ logger.error(f"CSV export failed: {e}")
285
+ return None, f"Export failed: {str(e)}"
286
+
287
+ @staticmethod
288
+ def export_to_json(history: List[Dict]) -> Tuple[Optional[str], str]:
289
+ """Export history to JSON"""
290
+ if not history:
291
+ return None, "No history to export"
292
+
293
+ try:
294
+ temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json', encoding='utf-8')
295
+ json.dump(history, temp_file, indent=2, ensure_ascii=False)
296
+ temp_file.close()
297
+
298
+ logger.info(f"Exported {len(history)} entries to JSON")
299
+ return temp_file.name, f"Exported {len(history)} entries to JSON"
300
+ except Exception as e:
301
+ logger.error(f"JSON export failed: {e}")
302
+ return None, f"Export failed: {str(e)}"
303
+
304
+ # Main application functions
305
+ def analyze_text(text: str, theme: str = 'default'):
306
+ """Main text analysis function"""
307
+ try:
308
+ if not text.strip():
309
+ return "Please enter text", None, None, None
310
+
311
+ result = SentimentAnalyzer.analyze_single(text)
312
 
313
  # Add to history
314
+ history_entry = {
315
  'text': text[:100],
316
  'full_text': text,
317
+ 'sentiment': result['sentiment'],
318
+ 'confidence': result['confidence'],
319
+ 'pos_prob': result['pos_prob'],
320
+ 'neg_prob': result['neg_prob'],
321
  'timestamp': datetime.now().isoformat()
322
+ }
323
+ history_manager.add_entry(history_entry)
324
+
325
+ # Create visualizations
326
+ probs = np.array([result['neg_prob'], result['pos_prob']])
327
+ prob_plot = Visualizer.create_probability_plot(probs, theme)
328
+ gauge_plot = Visualizer.create_confidence_gauge(result['confidence'], result['sentiment'], theme)
329
+ cloud_plot = Visualizer.create_wordcloud(text, result['sentiment'], theme)
330
+
331
+ result_text = f"Sentiment: {result['sentiment']} (Confidence: {result['confidence']:.3f})"
332
+ return result_text, prob_plot, gauge_plot, cloud_plot
333
+
334
+ except Exception as e:
335
+ logger.error(f"Text analysis failed: {e}")
336
+ return f"Analysis failed: {str(e)}", None, None, None
337
+
338
+ def batch_analysis(reviews: str, progress=gr.Progress()):
339
+ """Batch analysis function"""
340
+ try:
341
+ if not reviews.strip():
342
+ return None
343
+
344
+ texts = [r.strip() for r in reviews.split('\n') if r.strip()]
345
+ if len(texts) < 2:
346
+ return None
347
+
348
+ results = SentimentAnalyzer.analyze_batch(texts, progress)
349
+
350
+ # Add to history
351
+ for result in results:
352
+ history_entry = {
353
+ 'text': result['text'],
354
+ 'full_text': result['full_text'],
355
+ 'sentiment': result['sentiment'],
356
+ 'confidence': result['confidence'],
357
+ 'pos_prob': result['pos_prob'],
358
+ 'neg_prob': result['neg_prob'],
359
+ 'timestamp': datetime.now().isoformat()
360
+ }
361
+ history_manager.add_entry(history_entry)
362
+
363
+ # Create batch visualization
364
+ return create_batch_visualization(results)
365
+
366
+ except Exception as e:
367
+ logger.error(f"Batch analysis failed: {e}")
368
+ return None
369
+
370
+ def create_batch_visualization(results: List[Dict]) -> plt.Figure:
371
+ """Create comprehensive batch analysis visualization"""
372
  fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10))
373
 
374
+ # Sentiment distribution pie chart
375
  sent_counts = Counter([r['sentiment'] for r in results])
376
  colors = ['#4ecdc4', '#ff6b6b']
377
  ax1.pie(sent_counts.values(), labels=sent_counts.keys(),
 
385
  ax2.set_xlabel('Confidence')
386
  ax2.set_ylabel('Count')
387
 
388
+ # Probability scatter plot
389
  indices = range(len(results))
390
  pos_probs = [r['pos_prob'] for r in results]
391
  ax3.scatter(indices, pos_probs,
 
396
  ax3.set_xlabel('Review Index')
397
  ax3.set_ylabel('Positive Probability')
398
 
399
+ # Confidence vs Sentiment scatter
400
  sent_binary = [1 if r['sentiment'] == 'Positive' else 0 for r in results]
401
  ax4.scatter(confs, sent_binary, alpha=0.7, s=100,
402
  c=['#4ecdc4' if s == 1 else '#ff6b6b' for s in sent_binary])
 
409
  plt.tight_layout()
410
  return fig
411
 
412
+ def plot_history():
413
+ """Plot analysis history"""
414
+ history = history_manager.get_history()
415
+ if len(history) < 2:
416
+ return None, f"History contains {len(history)} entries. Need at least 2 for visualization."
417
+
418
+ try:
419
+ indices = list(range(len(history)))
420
+ pos_probs = [item['pos_prob'] for item in history]
421
+ confs = [item['confidence'] for item in history]
422
+
423
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
424
+
425
+ colors = ['#4ecdc4' if p > 0.5 else '#ff6b6b' for p in pos_probs]
426
+ ax1.scatter(indices, pos_probs, c=colors, alpha=0.7, s=100)
427
+ ax1.plot(indices, pos_probs, alpha=0.5, linewidth=2)
428
+ ax1.axhline(y=0.5, color='gray', linestyle='--', alpha=0.5)
429
+ ax1.set_title('Sentiment History - Positive Probability')
430
+ ax1.set_xlabel('Analysis Number')
431
+ ax1.set_ylabel('Positive Probability')
432
+ ax1.grid(True, alpha=0.3)
433
+
434
+ ax2.bar(indices, confs, alpha=0.7, color='lightblue', edgecolor='navy')
435
+ ax2.set_title('Confidence Scores Over Time')
436
+ ax2.set_xlabel('Analysis Number')
437
+ ax2.set_ylabel('Confidence Score')
438
+ ax2.grid(True, alpha=0.3)
439
+
440
+ plt.tight_layout()
441
+ return fig, f"History contains {len(history)} analyses"
442
+ except Exception as e:
443
+ logger.error(f"History plotting failed: {e}")
444
+ return None, f"Plotting failed: {str(e)}"
445
+
446
+ # File processing
447
  def process_uploaded_file(file):
448
+ """Process uploaded file"""
449
  if file is None:
450
  return ""
451
 
452
+ try:
453
+ content = file.read().decode('utf-8')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
 
455
+ if file.name.endswith('.csv'):
456
+ lines = content.split('\n')
457
+ if ',' in content:
458
+ reviews = []
459
+ reader = csv.reader(lines)
460
+ headers = next(reader, None)
461
+ if headers and any('review' in h.lower() for h in headers):
462
+ review_idx = next(i for i, h in enumerate(headers) if 'review' in h.lower())
463
+ for row in reader:
464
+ if len(row) > review_idx:
465
+ reviews.append(row[review_idx])
466
+ else:
467
+ for row in reader:
468
+ if row:
469
+ reviews.append(row[0])
470
+ return '\n'.join(reviews)
471
+
472
+ return content
473
+ except Exception as e:
474
+ logger.error(f"File processing failed: {e}")
475
+ return f"File processing failed: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
+ # Export functions
478
+ def export_csv():
479
+ return DataExporter.export_to_csv(history_manager.get_history())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
 
481
+ def export_json():
482
+ return DataExporter.export_to_json(history_manager.get_history())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
 
484
+ # Status functions
485
+ def get_history_status():
486
+ return f"History contains {history_manager.size()} entries"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
 
488
  def clear_history():
489
+ count = history_manager.clear()
 
 
 
490
  return f"Cleared {count} entries from history"
491
 
492
+ # Example data
 
 
 
 
493
  EXAMPLE_REVIEWS = [
494
  ["The cinematography was stunning, but the plot felt predictable and the dialogue was weak."],
495
  ["A masterpiece of filmmaking! Amazing performances, brilliant direction, and unforgettable moments."],
496
  ["Boring movie with terrible acting, weak plot, and poor character development throughout."],
497
  ["Great special effects and action sequences, but the story was confusing and hard to follow."],
498
+ ["Incredible ending that left me speechless! One of the best films I've ever seen."]
 
 
 
 
 
499
  ]
500
 
501
  # Gradio Interface
 
515
  with gr.Row():
516
  analyze_btn = gr.Button("Analyze", variant="primary", size="lg")
517
  theme_selector = gr.Dropdown(
518
+ choices=list(config.THEMES.keys()),
519
  value="default",
520
  label="Color Theme"
521
  )
 
537
 
538
  with gr.Tab("Batch Analysis"):
539
  gr.Markdown("### Multiple Reviews Analysis")
540
+ gr.Markdown(f"**Note:** Limited to {config.BATCH_SIZE_LIMIT} reviews per batch for optimal performance")
541
 
542
  with gr.Row():
543
  with gr.Column():
 
558
 
559
  batch_plot = gr.Plot(label="Batch Analysis Results")
560
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  with gr.Tab("History & Export"):
562
  gr.Markdown("### Analysis History & Data Export")
563
 
 
573
  history_status = gr.Textbox(label="Status", interactive=False)
574
  history_plot = gr.Plot(label="Historical Analysis Trends")
575
 
 
576
  csv_file_output = gr.File(label="Download CSV", visible=True)
577
  json_file_output = gr.File(label="Download JSON", visible=True)
578
 
 
595
  outputs=batch_plot
596
  )
597
 
 
 
 
 
598
  refresh_btn.click(
599
  plot_history,
600
  outputs=[history_plot, history_status]
 
611
  )
612
 
613
  export_csv_btn.click(
614
+ export_csv,
615
  outputs=[csv_file_output, history_status]
616
  )
617
 
618
  export_json_btn.click(
619
+ export_json,
620
  outputs=[json_file_output, history_status]
621
  )
622
 
623
  if __name__ == "__main__":
624
+ demo.launch(share=True)