Kartikay Khosla commited on
Commit
3e4638e
Β·
1 Parent(s): bfdf5dd

Deploy Streamlit app with Vertex AI Gemini

Browse files
Files changed (2) hide show
  1. app.py +171 -86
  2. requirements.txt +1 -7
app.py CHANGED
@@ -10,24 +10,41 @@ import torch
10
  from langdetect import detect
11
  import streamlit as st
12
  import io
13
- from newspaper import Article # βœ… for URL input
14
- import concurrent.futures # βœ… for safe timeout
15
 
16
- # ==== Gemini via API Key (AI Studio) ====
17
- import google.generativeai as genai
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- api_key = os.getenv("GEMINI_API_KEY")
20
- if not api_key:
21
- raise ValueError("❌ Missing GEMINI_API_KEY. Please set it as environment variable or in Hugging Face secrets.")
22
 
23
- # βœ… Configure Gemini with just the API key
24
- genai.configure(api_key=api_key)
25
 
26
- # βœ… Load Gemini model
27
- gemini_model = genai.GenerativeModel("gemini-pro")
 
 
 
 
 
28
 
29
  # ===============================
30
- # πŸ”§ Safe SpaCy + Stanza Downloads
31
  # ===============================
32
  def safe_load_spacy():
33
  try:
@@ -41,24 +58,28 @@ def safe_load_spacy():
41
 
42
  nlp_en = safe_load_spacy()
43
 
44
- # Ensure Stanza models exist
45
  stanza_dir = os.path.expanduser("~/.stanza_resources")
46
- if not os.path.exists(stanza_dir):
47
- stanza.download('hi')
48
- stanza.download('ta')
 
49
 
50
- stanza.download('hi')
51
- stanza.download('ta')
52
 
53
- nlp_hi = stanza.Pipeline('hi', processors='tokenize,pos', use_gpu=torch.cuda.is_available())
54
- nlp_ta = stanza.Pipeline('ta', processors='tokenize,pos', use_gpu=torch.cuda.is_available())
 
 
 
55
 
56
  # ===============================
57
- # Language-Aware Pipeline Loader
58
  # ===============================
59
  def load_pipelines(language_code):
60
  lang = language_code.upper()
61
  device = 0 if torch.cuda.is_available() else -1
 
62
  st.write(f"🌍 Language detected: {lang}")
63
  st.write(f"Device set to use {'cuda:0' if device == 0 else 'cpu'}")
64
 
@@ -97,7 +118,10 @@ def load_pipelines(language_code):
97
  # ===============================
98
  def read_and_split_articles(file_path):
99
  doc = docx.Document(file_path)
100
- paragraphs = [para.text.strip() for para in doc.paragraphs if para.text.strip()]
 
 
 
101
  return paragraphs
102
 
103
  # ===============================
@@ -107,35 +131,38 @@ def read_article_from_url(url):
107
  article = Article(url)
108
  article.download()
109
  article.parse()
110
- title = (article.title or "").strip()
111
- body = (article.text or "").strip()
112
- return f"{title}\n\n{body}".strip()
113
 
114
  # ===============================
115
- # Filter Neutral
116
  # ===============================
117
  def filter_neutral(emotion_results, neutral_threshold=0.75):
118
- scores = {r["label"]: round(r["score"], 3)
119
- for r in sorted(emotion_results, key=lambda x: x["score"], reverse=True)}
 
 
 
120
  if "neutral" in scores and scores["neutral"] > neutral_threshold:
121
  scores.pop("neutral")
 
122
  return scores
123
 
124
  # ===============================
125
- # Sentence Splitter
126
  # ===============================
127
  def split_sentences(text, lang):
128
  if lang == "hi":
129
- sentences = re.split(r'ΰ₯€', text)
 
130
  elif lang == "ta":
131
- sentences = re.split(r'\.', text)
 
132
  else:
133
  doc = nlp_en(text)
134
- sentences = [sent.text.strip() for sent in doc.sents]
135
- return [s.strip() for s in sentences if s.strip()]
136
 
137
  # ===============================
138
- # POS Tagger
139
  # ===============================
140
  def get_pos_tags(sentence, lang):
141
  if lang == "en":
@@ -143,10 +170,18 @@ def get_pos_tags(sentence, lang):
143
  return [(token.text, token.pos_) for token in doc]
144
  elif lang == "hi":
145
  doc = nlp_hi(sentence)
146
- return [(word.text, word.upos) for sent in doc.sentences for word in sent.words]
 
 
 
 
147
  elif lang == "ta":
148
  doc = nlp_ta(sentence)
149
- return [(word.text, word.upos) for sent in doc.sentences for word in sent.words]
 
 
 
 
150
  return []
151
 
152
  # ===============================
@@ -158,43 +193,70 @@ def normalize_scores(scores: dict):
158
  max_val = max(scores.values())
159
  if max_val == 0:
160
  return scores
161
- return {k: round(v / max_val, 3) for k, v in scores.items()}
 
 
 
162
 
163
  # ===============================
164
- # Gemini – Generate Insight (Safe Hard Timeout)
165
  # ===============================
166
  def generate_insight(text, emotions, sentiment, level="Paragraph"):
167
  try:
168
- top_emotions = sorted(emotions.items(), key=lambda x: x[1], reverse=True)[:3]
 
 
 
169
  emo_text = ", ".join([f"{k}: {v}" for k, v in top_emotions]) if top_emotions else "N/A"
170
- sent_text = f"{sentiment['label']} ({round(sentiment['score'], 3)})" if sentiment else "N/A"
171
-
172
- prompt = (
173
- f"{level} to analyze:\n\n{text}\n\n"
174
- f"Top 3 detected emotions (normalized 0–1): {emo_text}\n"
175
- f"Overall sentiment: {sent_text}\n\n"
176
- "You are an expert editor. Suggest concrete, content-specific rewrites and improvements "
177
- "to increase clarity, engagement, trust, and emotional impact. Keep it actionable and concise. "
178
- "Avoid repeating the original text; propose better alternatives."
179
- )
 
 
 
 
 
 
 
 
 
 
180
 
181
- # βœ… Run Gemini in background, kill after 15s
182
  with concurrent.futures.ThreadPoolExecutor() as executor:
183
- future = executor.submit(lambda: gemini_model.generate_content(prompt))
184
  try:
185
- response = future.result(timeout=15)
186
  except concurrent.futures.TimeoutError:
187
- return top_emotions, "⚠️ Gemini request timed out after 15s."
 
 
 
 
 
 
188
 
189
  if response and getattr(response, "text", None):
190
- return top_emotions, response.text.strip()
191
- return top_emotions, "⚠️ No insight generated."
 
 
 
 
 
 
192
 
193
  except Exception as e:
194
  return [], f"⚠️ Insight generation failed: {str(e)}"
195
 
196
  # ===============================
197
- # Analysis Function
198
  # ===============================
199
  def analyze_article(article_text, lang, emotion_pipeline, sentiment_pipeline):
200
  export_rows = []
@@ -203,7 +265,6 @@ def analyze_article(article_text, lang, emotion_pipeline, sentiment_pipeline):
203
  if len(paragraphs) <= 1:
204
  paragraphs = [p.strip() for p in article_text.split("\n") if p.strip()]
205
 
206
- # βœ… Debug: show how many paragraphs detected
207
  st.write(f"πŸ“‘ Paragraphs detected: {len(paragraphs)}")
208
 
209
  weighted_scores = {}
@@ -215,29 +276,40 @@ def analyze_article(article_text, lang, emotion_pipeline, sentiment_pipeline):
215
  for sentence in sentences:
216
  emo_results = emotion_pipeline(sentence[:512])[0]
217
  filtered = filter_neutral(emo_results)
 
218
  length = len(sentence.split())
219
  total_length += length
 
220
  for emo, score in filtered.items():
221
- weighted_scores[emo] = weighted_scores.get(emo, 0) + score * length
 
 
 
222
  senti_res = sentiment_pipeline(sentence[:512])[0]
223
- all_sentiments.append(max(senti_res, key=lambda x: x["score"]))
 
224
 
225
  if total_length > 0:
226
- weighted_scores = {emo: val / total_length for emo, val in weighted_scores.items()}
 
227
  weighted_scores = normalize_scores(weighted_scores)
228
- weighted_scores = dict(sorted(weighted_scores.items(), key=lambda x: x[1], reverse=True)[:10])
 
229
 
230
- overall_sentiment = max(all_sentiments, key=lambda x: x["score"]) if all_sentiments else {}
 
 
 
231
 
232
  st.subheader("πŸ“Š OVERALL (Weighted)")
233
  st.write("Emotions β†’", weighted_scores)
234
  st.write("Sentiment β†’", overall_sentiment)
235
 
236
  top3_overall, overall_insight = generate_insight(
237
- "Entire Article", weighted_scores, overall_sentiment, level="Overall Article"
238
  )
239
- st.write("πŸ”₯ Top 3 Emotions (for Gemini) β†’", dict(top3_overall))
240
- st.write("πŸ’‘ Overall Insight β†’", overall_insight)
241
 
242
  export_rows.append({
243
  "Type": "Overall",
@@ -255,24 +327,34 @@ def analyze_article(article_text, lang, emotion_pipeline, sentiment_pipeline):
255
  sentences = split_sentences(para, lang[:2])
256
  for sentence in sentences:
257
  results = emotion_pipeline(sentence[:512])[0]
258
- filtered = filter_neutral(results, neutral_threshold=0.75)
 
259
  for emo, score in filtered.items():
260
  para_counter[emo] += score
 
261
  senti_res = sentiment_pipeline(sentence[:512])[0]
262
- para_sentiments.append(max(senti_res, key=lambda x: x["score"]))
 
263
 
264
- para_emotions = dict(sorted(para_counter.items(), key=lambda x: x[1], reverse=True))
265
  para_emotions = normalize_scores(para_emotions)
266
- para_emotions = dict(list(para_emotions.items())[:10])
267
- para_sentiment = max(para_sentiments, key=lambda x: x["score"]) if para_sentiments else {}
 
 
 
 
 
268
 
269
  st.write(f"\nπŸ“‘ Paragraph {p_idx}: {para}")
270
  st.write("Emotions β†’", para_emotions)
271
  st.write("Sentiment β†’", para_sentiment)
272
 
273
- top3_para, insight = generate_insight(para, para_emotions, para_sentiment, level="Paragraph")
274
- st.write("πŸ”₯ Top 3 Emotions (for Gemini) β†’", dict(top3_para))
275
- st.write("πŸ’‘ Insights + Rewrites β†’", insight)
 
 
276
 
277
  export_rows.append({
278
  "Type": "Paragraph",
@@ -291,6 +373,7 @@ def analyze_article(article_text, lang, emotion_pipeline, sentiment_pipeline):
291
  st.title("πŸ“‘ Multilingual Text Emotion + Sentiment Analyzer")
292
 
293
  download_top = st.empty()
 
294
  uploaded_file = st.file_uploader("Upload a DOCX file", type=["docx"])
295
  url_input = st.text_input("Or enter an Article URL")
296
  text_input = st.text_area("Or paste text here")
@@ -298,18 +381,19 @@ text_input = st.text_area("Or paste text here")
298
  if st.button("πŸ” Analyze"):
299
  with st.spinner("Running analysis... ⏳"):
300
  if uploaded_file:
301
- doc_paras = read_and_split_articles(uploaded_file)
302
- text_to_analyze = "\n\n".join(doc_paras)
303
  elif url_input.strip():
304
  text_to_analyze = read_article_from_url(url_input)
305
  elif text_input.strip():
306
  text_to_analyze = text_input
307
  else:
308
- st.warning("Please upload a DOCX, enter a URL, or paste text to analyze.")
309
  st.stop()
310
 
311
  detected_lang = detect(text_to_analyze[:200]) if text_to_analyze else "en"
 
312
  emotion_pipeline, sentiment_pipeline = load_pipelines(detected_lang)
 
313
  export_rows = analyze_article(text_to_analyze, detected_lang, emotion_pipeline, sentiment_pipeline)
314
 
315
  df_export = pd.DataFrame(export_rows)
@@ -317,18 +401,19 @@ if st.button("πŸ” Analyze"):
317
 
318
  with download_top.container():
319
  st.download_button(
320
- label="⬇️ Download CSV",
321
- data=csv,
322
- file_name="analysis_results.csv",
323
- mime="text/csv",
324
- use_container_width=True,
325
  )
 
326
  excel_buffer = io.BytesIO()
327
  df_export.to_excel(excel_buffer, index=False, engine="xlsxwriter")
328
  st.download_button(
329
- label="⬇️ Download Excel",
330
- data=excel_buffer,
331
- file_name="analysis_results.xlsx",
332
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
333
- use_container_width=True,
334
  )
 
10
  from langdetect import detect
11
  import streamlit as st
12
  import io
13
+ from newspaper import Article
14
+ import concurrent.futures
15
 
16
+ # ===============================
17
+ # πŸ”‘ Vertex AI Setup
18
+ # ===============================
19
+ import vertexai
20
+ from vertexai.preview.generative_models import GenerativeModel
21
+
22
+ import json
23
+ import tempfile
24
+
25
+ if "GCP_SERVICE_ACCOUNT_JSON" not in os.environ:
26
+ raise RuntimeError("❌ GCP_SERVICE_ACCOUNT_JSON secret not found in Hugging Face Space")
27
+
28
+ # Write the JSON secret into a temp file
29
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as f:
30
+ f.write(os.environ["GCP_SERVICE_ACCOUNT_JSON"].encode("utf-8"))
31
+ SERVICE_ACCOUNT_PATH = f.name
32
 
33
+ os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = SERVICE_ACCOUNT_PATH
 
 
34
 
35
+ PROJECT_ID = "prod-project-jnm-smart-cms"
36
+ REGION = "us-central1"
37
 
38
+ vertexai.init(project=PROJECT_ID, location=REGION)
39
+
40
+ try:
41
+ gemini_model = GenerativeModel("gemini-2.5-pro")
42
+ except Exception as e:
43
+ st.warning(f"⚠️ Falling back to gemini-2.5-flash due to: {e}")
44
+ gemini_model = GenerativeModel("gemini-2.5-flash")
45
 
46
  # ===============================
47
+ # Safe SpaCy + Stanza Loads
48
  # ===============================
49
  def safe_load_spacy():
50
  try:
 
58
 
59
  nlp_en = safe_load_spacy()
60
 
 
61
  stanza_dir = os.path.expanduser("~/.stanza_resources")
62
+ if not os.path.exists(os.path.join(stanza_dir, "hi")):
63
+ stanza.download("hi")
64
+ if not os.path.exists(os.path.join(stanza_dir, "ta")):
65
+ stanza.download("ta")
66
 
67
+ nlp_hi = stanza.Pipeline("hi", processors="tokenize,pos", use_gpu=torch.cuda.is_available())
68
+ nlp_ta = stanza.Pipeline("ta", processors="tokenize,pos", use_gpu=torch.cuda.is_available())
69
 
70
+ # ===============================
71
+ # Streamlit run check
72
+ # ===============================
73
+ if not hasattr(st, "runtime") or not getattr(st.runtime, "exists", lambda: False)():
74
+ print("\n⚠️ WARNING: Run with `streamlit run app.py` instead of `python app.py`\n")
75
 
76
  # ===============================
77
+ # Load Hugging Face Pipelines
78
  # ===============================
79
  def load_pipelines(language_code):
80
  lang = language_code.upper()
81
  device = 0 if torch.cuda.is_available() else -1
82
+
83
  st.write(f"🌍 Language detected: {lang}")
84
  st.write(f"Device set to use {'cuda:0' if device == 0 else 'cpu'}")
85
 
 
118
  # ===============================
119
  def read_and_split_articles(file_path):
120
  doc = docx.Document(file_path)
121
+ paragraphs = []
122
+ for para in doc.paragraphs:
123
+ if para.text.strip():
124
+ paragraphs.append(para.text.strip())
125
  return paragraphs
126
 
127
  # ===============================
 
131
  article = Article(url)
132
  article.download()
133
  article.parse()
134
+ return f"{article.title.strip()}\n\n{article.text.strip()}"
 
 
135
 
136
  # ===============================
137
+ # Filter Neutral Emotions
138
  # ===============================
139
  def filter_neutral(emotion_results, neutral_threshold=0.75):
140
+ sorted_results = sorted(emotion_results, key=lambda x: x["score"], reverse=True)
141
+ scores = {}
142
+ for r in sorted_results:
143
+ scores[r["label"]] = round(r["score"], 3)
144
+
145
  if "neutral" in scores and scores["neutral"] > neutral_threshold:
146
  scores.pop("neutral")
147
+
148
  return scores
149
 
150
  # ===============================
151
+ # Split Sentences
152
  # ===============================
153
  def split_sentences(text, lang):
154
  if lang == "hi":
155
+ sentences = re.split(r"ΰ₯€", text)
156
+ return [s.strip() for s in sentences if s.strip()]
157
  elif lang == "ta":
158
+ sentences = re.split(r"\.", text)
159
+ return [s.strip() for s in sentences if s.strip()]
160
  else:
161
  doc = nlp_en(text)
162
+ return [sent.text.strip() for sent in doc.sents if sent.text.strip()]
 
163
 
164
  # ===============================
165
+ # POS Tagging
166
  # ===============================
167
  def get_pos_tags(sentence, lang):
168
  if lang == "en":
 
170
  return [(token.text, token.pos_) for token in doc]
171
  elif lang == "hi":
172
  doc = nlp_hi(sentence)
173
+ tags = []
174
+ for sent in doc.sentences:
175
+ for word in sent.words:
176
+ tags.append((word.text, word.upos))
177
+ return tags
178
  elif lang == "ta":
179
  doc = nlp_ta(sentence)
180
+ tags = []
181
+ for sent in doc.sentences:
182
+ for word in sent.words:
183
+ tags.append((word.text, word.upos))
184
+ return tags
185
  return []
186
 
187
  # ===============================
 
193
  max_val = max(scores.values())
194
  if max_val == 0:
195
  return scores
196
+ normalized = {}
197
+ for k, v in scores.items():
198
+ normalized[k] = round(v / max_val, 3)
199
+ return normalized
200
 
201
  # ===============================
202
+ # Gemini Insight Generation
203
  # ===============================
204
  def generate_insight(text, emotions, sentiment, level="Paragraph"):
205
  try:
206
+ filtered = {k: v for k, v in emotions.items() if k.lower() != "neutral"}
207
+ sorted_emotions = sorted(filtered.items(), key=lambda x: x[1], reverse=True)
208
+ top_emotions = sorted_emotions[:3]
209
+
210
  emo_text = ", ".join([f"{k}: {v}" for k, v in top_emotions]) if top_emotions else "N/A"
211
+ sent_text = f"{sentiment.get('label','N/A')} ({round(sentiment.get('score',0), 3)})" if sentiment else "N/A"
212
+
213
+ prompt = f"""
214
+ You are an editorial coach.
215
+
216
+ Analyze this {level} and propose a rewrite.
217
+
218
+ Content:
219
+ {text}
220
+
221
+ Detected Top Emotions β†’ {emo_text}
222
+ Detected Sentiment β†’ {sent_text}
223
+
224
+ Your Output (concise):
225
+ - πŸ”₯ Suggested Rewrite (≀3 sentences, avoid repetition)
226
+ - πŸ’‘ Why it Works (≀2 sentences, tie directly to emotions/sentiment)
227
+ """
228
+
229
+ def call_model(model):
230
+ return model.generate_content(prompt)
231
 
 
232
  with concurrent.futures.ThreadPoolExecutor() as executor:
233
+ future = executor.submit(lambda: call_model(gemini_model))
234
  try:
235
+ response = future.result(timeout=40)
236
  except concurrent.futures.TimeoutError:
237
+ try:
238
+ flash_model = GenerativeModel("gemini-2.5-flash")
239
+ st.warning("⚑ Retrying with Flash due to Pro timeout...")
240
+ future = executor.submit(lambda: call_model(flash_model))
241
+ response = future.result(timeout=30)
242
+ except Exception:
243
+ return top_emotions, f"⚠️ Gemini request timed out.\n\nDetected Emotions: {emo_text}, Sentiment: {sent_text}"
244
 
245
  if response and getattr(response, "text", None):
246
+ final_text = (
247
+ f"πŸ”₯ Top 3 Emotions: {emo_text}\n"
248
+ f"πŸŒ“ Sentiment: {sent_text}\n\n"
249
+ f"{response.text.strip()}"
250
+ )
251
+ return top_emotions, final_text
252
+ else:
253
+ return top_emotions, f"⚠️ No insight generated.\n\nDetected Emotions: {emo_text}, Sentiment: {sent_text}"
254
 
255
  except Exception as e:
256
  return [], f"⚠️ Insight generation failed: {str(e)}"
257
 
258
  # ===============================
259
+ # Main Analyzer
260
  # ===============================
261
  def analyze_article(article_text, lang, emotion_pipeline, sentiment_pipeline):
262
  export_rows = []
 
265
  if len(paragraphs) <= 1:
266
  paragraphs = [p.strip() for p in article_text.split("\n") if p.strip()]
267
 
 
268
  st.write(f"πŸ“‘ Paragraphs detected: {len(paragraphs)}")
269
 
270
  weighted_scores = {}
 
276
  for sentence in sentences:
277
  emo_results = emotion_pipeline(sentence[:512])[0]
278
  filtered = filter_neutral(emo_results)
279
+
280
  length = len(sentence.split())
281
  total_length += length
282
+
283
  for emo, score in filtered.items():
284
+ if emo not in weighted_scores:
285
+ weighted_scores[emo] = 0
286
+ weighted_scores[emo] += score * length
287
+
288
  senti_res = sentiment_pipeline(sentence[:512])[0]
289
+ best_senti = max(senti_res, key=lambda x: x["score"])
290
+ all_sentiments.append(best_senti)
291
 
292
  if total_length > 0:
293
+ for emo in weighted_scores:
294
+ weighted_scores[emo] = weighted_scores[emo] / total_length
295
  weighted_scores = normalize_scores(weighted_scores)
296
+ sorted_scores = sorted(weighted_scores.items(), key=lambda x: x[1], reverse=True)
297
+ weighted_scores = dict(sorted_scores[:10])
298
 
299
+ if all_sentiments:
300
+ overall_sentiment = max(all_sentiments, key=lambda x: x["score"])
301
+ else:
302
+ overall_sentiment = {}
303
 
304
  st.subheader("πŸ“Š OVERALL (Weighted)")
305
  st.write("Emotions β†’", weighted_scores)
306
  st.write("Sentiment β†’", overall_sentiment)
307
 
308
  top3_overall, overall_insight = generate_insight(
309
+ article_text, weighted_scores, overall_sentiment, "Overall Article"
310
  )
311
+
312
+ st.write(overall_insight)
313
 
314
  export_rows.append({
315
  "Type": "Overall",
 
327
  sentences = split_sentences(para, lang[:2])
328
  for sentence in sentences:
329
  results = emotion_pipeline(sentence[:512])[0]
330
+ filtered = filter_neutral(results)
331
+
332
  for emo, score in filtered.items():
333
  para_counter[emo] += score
334
+
335
  senti_res = sentiment_pipeline(sentence[:512])[0]
336
+ best_senti = max(senti_res, key=lambda x: x["score"])
337
+ para_sentiments.append(best_senti)
338
 
339
+ para_emotions = dict(para_counter)
340
  para_emotions = normalize_scores(para_emotions)
341
+ sorted_para = sorted(para_emotions.items(), key=lambda x: x[1], reverse=True)
342
+ para_emotions = dict(sorted_para[:10])
343
+
344
+ if para_sentiments:
345
+ para_sentiment = max(para_sentiments, key=lambda x: x["score"])
346
+ else:
347
+ para_sentiment = {}
348
 
349
  st.write(f"\nπŸ“‘ Paragraph {p_idx}: {para}")
350
  st.write("Emotions β†’", para_emotions)
351
  st.write("Sentiment β†’", para_sentiment)
352
 
353
+ top3_para, insight = generate_insight(
354
+ para, para_emotions, para_sentiment, "Paragraph"
355
+ )
356
+
357
+ st.write(insight)
358
 
359
  export_rows.append({
360
  "Type": "Paragraph",
 
373
  st.title("πŸ“‘ Multilingual Text Emotion + Sentiment Analyzer")
374
 
375
  download_top = st.empty()
376
+
377
  uploaded_file = st.file_uploader("Upload a DOCX file", type=["docx"])
378
  url_input = st.text_input("Or enter an Article URL")
379
  text_input = st.text_area("Or paste text here")
 
381
  if st.button("πŸ” Analyze"):
382
  with st.spinner("Running analysis... ⏳"):
383
  if uploaded_file:
384
+ text_to_analyze = "\n\n".join(read_and_split_articles(uploaded_file))
 
385
  elif url_input.strip():
386
  text_to_analyze = read_article_from_url(url_input)
387
  elif text_input.strip():
388
  text_to_analyze = text_input
389
  else:
390
+ st.warning("Please provide text input.")
391
  st.stop()
392
 
393
  detected_lang = detect(text_to_analyze[:200]) if text_to_analyze else "en"
394
+
395
  emotion_pipeline, sentiment_pipeline = load_pipelines(detected_lang)
396
+
397
  export_rows = analyze_article(text_to_analyze, detected_lang, emotion_pipeline, sentiment_pipeline)
398
 
399
  df_export = pd.DataFrame(export_rows)
 
401
 
402
  with download_top.container():
403
  st.download_button(
404
+ "⬇️ Download CSV",
405
+ csv,
406
+ "analysis_results.csv",
407
+ "text/csv",
408
+ use_container_width=True
409
  )
410
+
411
  excel_buffer = io.BytesIO()
412
  df_export.to_excel(excel_buffer, index=False, engine="xlsxwriter")
413
  st.download_button(
414
+ "⬇️ Download Excel",
415
+ excel_buffer,
416
+ "analysis_results.xlsx",
417
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
418
+ use_container_width=True
419
  )
requirements.txt CHANGED
@@ -1,4 +1,3 @@
1
- # Core app
2
  streamlit
3
  pandas
4
  torch
@@ -11,10 +10,5 @@ openpyxl
11
  xlsxwriter
12
  lxml[html_clean]
13
  newspaper3k==0.2.8
 
14
 
15
- # Gemini (AI Studio API key mode only)
16
- google-generativeai>=0.3.0
17
-
18
- # βœ… SpaCy + English model
19
- spacy>=3.7.0
20
- https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.1/en_core_web_sm-3.7.1-py3-none-any.whl
 
 
1
  streamlit
2
  pandas
3
  torch
 
10
  xlsxwriter
11
  lxml[html_clean]
12
  newspaper3k==0.2.8
13
+ google-cloud-aiplatform>=1.66.0
14