shiue2000 commited on
Commit
bd78a06
·
verified ·
1 Parent(s): f159a88

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +86 -112
app.py CHANGED
@@ -10,19 +10,19 @@ from datetime import datetime, timedelta
10
  import gradio as gr
11
  import logging
12
  from jinja2 import Template
13
- # ===== 字型與樣式 =====
14
  plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
15
  plt.rcParams['axes.unicode_minus'] = False
16
  plt.style.use("seaborn-v0_8")
17
- # ===== 日誌 =====
18
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
19
- # ===== 參數 =====
20
  candidates = ["許智傑", "邱議瑩", "賴瑞隆", "林岱樺", "柯志恩"]
21
  days_back = 7
22
  max_tweets_per_candidate = 20
23
  news_file = "news_sample.csv"
24
  history_file = "history_sentiment.csv"
25
- # ===== 情緒分析 =====
26
  try:
27
  from transformers import pipeline
28
  sentiment_pipeline = pipeline(
@@ -37,25 +37,25 @@ except:
37
  "label": random.choice(["positive", "negative", "neutral"]),
38
  "score": random.uniform(0.3, 0.9)
39
  }
40
- # ===== 模擬貼文抓取 =====
41
  def fetch_tweets(candidate):
42
  sample_texts = {
43
- "許智傑": ["許智傑積極參與地方活動", "許智傑被指政策空洞", "支持許智傑,打造高雄新未來!"],
44
- "邱議瑩": ["邱議瑩強勢表態選市長", "邱議瑩林岱樺", "邱議瑩推客家文化"],
45
- "賴瑞隆": ["賴瑞隆推海洋經濟", "賴瑞隆民調領先", "賴瑞隆被質疑經驗不足"],
46
- "林岱樺": ["林岱樺積極跑基層", "林岱樺涉助理費爭議", "林岱樺獲正國會支持"],
47
- "柯志恩": ["柯志恩民調大幅領先", "柯志恩被批勘災缺席", "柯志恩推青年政策"]
48
  }
49
  return pd.DataFrame([
50
  {
51
  "Date": datetime.now() - timedelta(days=random.randint(0, days_back - 1)),
52
  "User": f"user{random.randint(1, 100)}",
53
- "Content": random.choice(sample_texts.get(candidate, [f"{candidate} 的貼文 {i}"])),
54
  "Candidate": candidate
55
  }
56
  for i in range(random.randint(5, max_tweets_per_candidate))
57
  ])
58
- # ===== 工具: Matplotlib base64 =====
59
  def fig_to_base64():
60
  buf = io.BytesIO()
61
  plt.savefig(buf, format="png", dpi=120, bbox_inches="tight")
@@ -64,103 +64,76 @@ def fig_to_base64():
64
  buf.close()
65
  plt.close()
66
  return img_b64
67
- # ===== 多圖產生器 =====
68
  def generate_charts(all_df, summary, df_hist):
69
  results = {}
70
- # 1. 每日情緒比例
71
- # ===== 字型與樣式 =====
72
- plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
73
- plt.rcParams['axes.unicode_minus'] = False
74
- plt.style.use("seaborn-v0_8")
75
-
76
  fig = plt.figure(figsize=(8, 5))
77
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
78
  kind='bar', stacked=True, colormap='coolwarm', ax=fig.gca()
79
  )
80
- plt.title("候選人每日社群情緒比例")
81
- plt.ylabel("比例")
82
- plt.xlabel("候選人")
 
83
  results["img_b64_today"] = fig_to_base64()
84
- # 2. 歷史情緒趨勢
85
- # ===== 字型與樣式 =====
86
- plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
87
- plt.rcParams['axes.unicode_minus'] = False
88
- plt.style.use("seaborn-v0_8")
89
-
90
  fig = plt.figure(figsize=(10, 5))
91
  for c in candidates:
92
  temp = df_hist[df_hist['Candidate'] == c]
93
  if not temp.empty:
94
- plt.plot(temp['Date'], temp['Positive Ratio'], marker='o', label=f"{c} 正面")
95
- plt.plot(temp['Date'], temp['Negative Ratio'], marker='x', label=f"{c} 負面")
96
- plt.plot(temp['Date'], temp['Neutral Ratio'], marker='s', label=f"{c} 中性")
97
- plt.title("候選人歷史情緒趨勢")
98
  plt.xticks(rotation=45)
99
- plt.ylabel("比例")
 
100
  plt.legend()
101
  results["img_b64_trend"] = fig_to_base64()
102
- # 3. 社群情緒趨勢
103
- # ===== 字型與樣式 =====
104
- plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
105
- plt.rcParams['axes.unicode_minus'] = False
106
- plt.style.use("seaborn-v0_8")
107
-
108
  sentiment_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Sentiment']).size().unstack(fill_value=0)
109
  sentiment_trend = sentiment_trend.div(sentiment_trend.sum(axis=1), axis=0).fillna(0)
110
  fig = plt.figure(figsize=(8, 5))
111
  for s in ['positive', 'negative', 'neutral']:
112
  if s in sentiment_trend.columns:
113
  plt.plot(sentiment_trend.index, sentiment_trend[s], marker='o', label=s.capitalize())
114
- plt.title("社群情緒趨勢")
115
- plt.xlabel("日期")
116
- plt.ylabel("比例")
117
  plt.legend()
118
  results["img_social_sentiment"] = fig_to_base64()
119
- # 4. 平台表現
120
- # ===== 字型與樣式 =====
121
- plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
122
- plt.rcParams['axes.unicode_minus'] = False
123
- plt.style.use("seaborn-v0_8")
124
-
125
  platforms = ["X", "Facebook", "Instagram", "PTT", "Line"]
126
  platform_counts = pd.Series({p: random.randint(10, 100) for p in platforms})
127
  fig = plt.figure(figsize=(8, 5))
128
  plt.bar(platforms, platform_counts, color='skyblue')
129
- plt.title("平台貼文聲量")
130
- plt.xlabel("平台")
131
- plt.ylabel("貼文數量")
132
  results["img_platform_performance"] = fig_to_base64()
133
- # 5. 候選人聲量趨勢
134
- # ===== 字型與樣式 =====
135
- plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
136
- plt.rcParams['axes.unicode_minus'] = False
137
- plt.style.use("seaborn-v0_8")
138
-
139
  candidate_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Candidate']).size().unstack(fill_value=0)
140
  fig = plt.figure(figsize=(8, 5))
141
  for c in candidates:
142
  if c in candidate_trend.columns:
143
  plt.plot(candidate_trend.index, candidate_trend[c], marker='o', label=c)
144
- plt.title("候選人貼文聲量趨勢")
145
- plt.xlabel("日期")
146
- plt.ylabel("貼文數量")
147
  plt.legend()
148
  results["img_candidate_volume"] = fig_to_base64()
149
- # 6. 候選人情緒分析
150
- # ===== 字型與樣式 =====
151
- plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
152
- plt.rcParams['axes.unicode_minus'] = False
153
- plt.style.use("seaborn-v0_8")
154
-
155
  fig = plt.figure(figsize=(8, 5))
156
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
157
  kind='bar', stacked=True, colormap='coolwarm', ax=fig.gca()
158
  )
159
- plt.title("候選人貼文情緒分析(正//中性)")
160
- plt.ylabel("比例")
161
- plt.xlabel("候選人")
 
162
  results["img_candidate_sentiment"] = fig_to_base64()
163
- # 7. 知識圖譜
164
  fig, ax = plt.subplots(figsize=(8, 6))
165
  G = nx.Graph()
166
  for c in candidates:
@@ -168,9 +141,10 @@ def generate_charts(all_df, summary, df_hist):
168
  for i in range(len(candidates) - 1):
169
  G.add_edge(candidates[i], candidates[i + 1])
170
  nx.draw(G, nx.spring_layout(G), with_labels=True, node_color='lightgreen', font_size=12, ax=ax)
 
171
  results["img_knowledge_graph"] = fig_to_base64()
172
  return results
173
- # ===== 主分析函數 =====
174
  def run_analysis():
175
  try:
176
  # Embed the template as a string to avoid file dependency and ensure syntax is correct
@@ -180,62 +154,62 @@ def run_analysis():
180
  <head>
181
  <meta charset="UTF-8">
182
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
183
- <title>2026 高雄市長選舉輿情分析報告</title>
184
  <script src="https://cdn.tailwindcss.com"></script>
185
  </head>
186
  <body class="bg-gray-100 font-sans leading-normal tracking-normal">
187
  <div class="container mx-auto p-4">
188
- <h1 class="text-3xl font-bold mb-4">2026 高雄市長選舉輿情分析報告</h1>
189
- <p class="mb-4">報告日期: {{ report_date }}</p>
190
 
191
- <h2 class="text-2xl font-bold mb-2">參與度摘要</h2>
192
  {{ engagement_table | safe }}
193
 
194
- <h2 class="text-2xl font-bold mb-2">新聞摘要</h2>
195
  <ul class="list-disc pl-5 mb-4">
196
  {% for key, value in news_summary %}
197
  <li><strong>{{ key }}</strong>: {{ value }}</li>
198
  {% endfor %}
199
  </ul>
200
 
201
- <h2 class="text-2xl font-bold mb-2">新聞詳情</h2>
202
  {{ news_table | safe }}
203
 
204
- <h2 class="text-2xl font-bold mb-2">今日情緒比例</h2>
205
- <img src="data:image/png;base64,{{ img_b64_today }}" alt="今日情緒比例" class="mb-4">
206
 
207
- <h2 class="text-2xl font-bold mb-2">歷史情緒趨勢</h2>
208
- <img src="data:image/png;base64,{{ img_b64_trend }}" alt="歷史情緒趨勢" class="mb-4">
209
 
210
- <h2 class="text-2xl font-bold mb-2">社群情緒趨勢</h2>
211
- <img src="data:image/png;base64,{{ img_social_sentiment }}" alt="社群情緒趨勢" class="mb-4">
212
 
213
- <h2 class="text-2xl font-bold mb-2">平台表現</h2>
214
- <img src="data:image/png;base64,{{ img_platform_performance }}" alt="平台表現" class="mb-4">
215
 
216
- <h2 class="text-2xl font-bold mb-2">候選人聲量趨勢</h2>
217
- <img src="data:image/png;base64,{{ img_candidate_volume }}" alt="候選人聲量趨勢" class="mb-4">
218
 
219
- <h2 class="text-2xl font-bold mb-2">候選人情緒分析</h2>
220
- <img src="data:image/png;base64,{{ img_candidate_sentiment }}" alt="候選人情緒分析" class="mb-4">
221
 
222
- <h2 class="text-2xl font-bold mb-2">知識圖譜</h2>
223
- <img src="data:image/png;base64,{{ img_knowledge_graph }}" alt="知識圖譜" class="mb-4">
224
  </div>
225
  </body>
226
  </html>
227
  """
228
- # --- 貼文 & 情緒分析 ---
229
  all_df = pd.concat([fetch_tweets(c) for c in candidates], ignore_index=True)
230
  all_df['Sentiment'] = all_df['Content'].apply(lambda x: sentiment(x)['label'])
231
  all_df['Confidence'] = all_df['Content'].apply(lambda x: sentiment(x)['score'])
232
- # --- 統計 ---
233
  summary = all_df.groupby(['Candidate', 'Sentiment']).size().unstack(fill_value=0)
234
  summary['Total Posts'] = summary.sum(axis=1)
235
  summary['Positive Ratio'] = summary.get('positive', 0) / summary['Total Posts'].replace(0, 1)
236
  summary['Negative Ratio'] = summary.get('negative', 0) / summary['Total Posts'].replace(0, 1)
237
  summary['Neutral Ratio'] = summary.get('neutral', 0) / summary['Total Posts'].replace(0, 1)
238
- # --- 歷史資料 ---
239
  today_str = datetime.now().strftime('%Y-%m-%d')
240
  hist_row = summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].copy()
241
  hist_row['Date'] = today_str
@@ -245,56 +219,56 @@ def run_analysis():
245
  ignore_index=True
246
  ) if os.path.exists(history_file) else hist_row
247
  df_hist.to_csv(history_file, index=False)
248
- # --- 圖表 ---
249
  charts = generate_charts(all_df, summary, df_hist)
250
- # --- 新聞 ---
251
  if os.path.exists(news_file):
252
  df_news = pd.read_csv(news_file)
253
  news_summary = df_news.groupby('Category').size().to_dict()
254
  news_table = df_news.to_html(index=False, classes="min-w-full border border-gray-200")
255
  else:
256
  news_summary = {
257
- "民調": "柯志恩在多份民調中領先綠營候選人。",
258
- "黨內競爭": "民進黨初選競爭激烈。",
259
- "爭議": "林岱樺涉助理費爭議。"
260
  }
261
- news_table = "<p>無新聞資料</p>"
262
  # Convert news_summary to list of tuples to support iteration in template
263
  news_summary = list(news_summary.items())
264
- # --- 參與表 ---
265
  engagement_table = f"""
266
  <table class="min-w-full bg-white border border-gray-200">
267
  <tr class="bg-gray-100 border-b">
268
- <th class="py-2 px-4 border-r">總參與度</th>
269
  <td class="py-2 px-4 border-r">{len(all_df)}</td>
270
- <th class="py-2 px-4 border-r">正面情緒比例</th>
271
  <td class="py-2 px-4 border-r">{all_df['Sentiment'].value_counts(normalize=True).get('positive', 0):.1%}</td>
272
- <th class="py-2 px-4 border-r">平均互動率</th>
273
  <td class="py-2 px-4 border-r">3.9%</td>
274
- <th class="py-2 px-4 border-r">活躍平台數</th>
275
  <td class="py-2 px-4">{5}</td>
276
  </tr>
277
  </table>
278
  """
279
- # --- HTML 渲染 ---
280
  template = Template(html_template)
281
  html_content = template.render(
282
  report_date=datetime.now().strftime('%Y-%m-%d %H:%M'),
283
- engagement_table=engagement_table if engagement_table else "<p>未提供互動數據</p>",
284
- news_summary=news_summary if news_summary else "<p>未提供新聞摘要</p>",
285
- news_table=news_table if news_table else "<p>未提供新聞資料</p>",
286
  **charts
287
  )
288
 
289
  return html_content
290
  except Exception:
291
- return f"<pre>❌ 分析失敗:\n{traceback.format_exc()}</pre>"
292
- # ===== Gradio 前端 =====
293
  if __name__ == "__main__":
294
  iface = gr.Interface(
295
  fn=run_analysis,
296
  inputs=[],
297
  outputs=gr.HTML(),
298
- title="2026 高雄市長選舉輿情分析"
299
  )
300
  iface.launch(server_name="0.0.0.0", server_port=7860)
 
10
  import gradio as gr
11
  import logging
12
  from jinja2 import Template
13
+ # ===== Font and Style Settings =====
14
  plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
15
  plt.rcParams['axes.unicode_minus'] = False
16
  plt.style.use("seaborn-v0_8")
17
+ # ===== Logging =====
18
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
19
+ # ===== Parameters =====
20
  candidates = ["許智傑", "邱議瑩", "賴瑞隆", "林岱樺", "柯志恩"]
21
  days_back = 7
22
  max_tweets_per_candidate = 20
23
  news_file = "news_sample.csv"
24
  history_file = "history_sentiment.csv"
25
+ # ===== Sentiment Analysis =====
26
  try:
27
  from transformers import pipeline
28
  sentiment_pipeline = pipeline(
 
37
  "label": random.choice(["positive", "negative", "neutral"]),
38
  "score": random.uniform(0.3, 0.9)
39
  }
40
+ # ===== Simulate Tweet Fetching =====
41
  def fetch_tweets(candidate):
42
  sample_texts = {
43
+ "許智傑": ["許智傑 actively participates in local events", "許智傑 criticized for vague policies", "Support 許智傑 for Kaohsiung's future!"],
44
+ "邱議瑩": ["邱議瑩 strongly announces mayoral candidacy", "邱議瑩 criticizes 林岱樺", "邱議瑩 promotes Hakka culture"],
45
+ "賴瑞隆": ["賴瑞隆 pushes marine economy", "賴瑞隆 leads in polls", "賴瑞隆 questioned for lack of experience"],
46
+ "林岱樺": ["林岱樺 actively engages grassroots", "林岱樺 involved in assistant fee controversy", "林岱樺 backed by New Tide faction"],
47
+ "柯志恩": ["柯志恩 leads significantly in polls", "柯志恩 criticized for absence during disaster inspection", "柯志恩 promotes youth policies"]
48
  }
49
  return pd.DataFrame([
50
  {
51
  "Date": datetime.now() - timedelta(days=random.randint(0, days_back - 1)),
52
  "User": f"user{random.randint(1, 100)}",
53
+ "Content": random.choice(sample_texts.get(candidate, [f"{candidate}'s post {i}"])),
54
  "Candidate": candidate
55
  }
56
  for i in range(random.randint(5, max_tweets_per_candidate))
57
  ])
58
+ # ===== Utility: Matplotlib to Base64 =====
59
  def fig_to_base64():
60
  buf = io.BytesIO()
61
  plt.savefig(buf, format="png", dpi=120, bbox_inches="tight")
 
64
  buf.close()
65
  plt.close()
66
  return img_b64
67
+ # ===== Chart Generator =====
68
  def generate_charts(all_df, summary, df_hist):
69
  results = {}
70
+ # 1. Daily Sentiment Ratio
 
 
 
 
 
71
  fig = plt.figure(figsize=(8, 5))
72
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
73
  kind='bar', stacked=True, colormap='coolwarm', ax=fig.gca()
74
  )
75
+ plt.title("Daily Sentiment Ratio by Candidate")
76
+ plt.ylabel("Ratio")
77
+ plt.xlabel("Candidate")
78
+ plt.legend(["Positive", "Negative", "Neutral"])
79
  results["img_b64_today"] = fig_to_base64()
80
+ # 2. Historical Sentiment Trend
 
 
 
 
 
81
  fig = plt.figure(figsize=(10, 5))
82
  for c in candidates:
83
  temp = df_hist[df_hist['Candidate'] == c]
84
  if not temp.empty:
85
+ plt.plot(temp['Date'], temp['Positive Ratio'], marker='o', label=f"{c} Positive")
86
+ plt.plot(temp['Date'], temp['Negative Ratio'], marker='x', label=f"{c} Negative")
87
+ plt.plot(temp['Date'], temp['Neutral Ratio'], marker='s', label=f"{c} Neutral")
88
+ plt.title("Historical Sentiment Trend by Candidate")
89
  plt.xticks(rotation=45)
90
+ plt.ylabel("Ratio")
91
+ plt.xlabel("Date")
92
  plt.legend()
93
  results["img_b64_trend"] = fig_to_base64()
94
+ # 3. Social Sentiment Trend
 
 
 
 
 
95
  sentiment_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Sentiment']).size().unstack(fill_value=0)
96
  sentiment_trend = sentiment_trend.div(sentiment_trend.sum(axis=1), axis=0).fillna(0)
97
  fig = plt.figure(figsize=(8, 5))
98
  for s in ['positive', 'negative', 'neutral']:
99
  if s in sentiment_trend.columns:
100
  plt.plot(sentiment_trend.index, sentiment_trend[s], marker='o', label=s.capitalize())
101
+ plt.title("Social Sentiment Trend")
102
+ plt.xlabel("Date")
103
+ plt.ylabel("Ratio")
104
  plt.legend()
105
  results["img_social_sentiment"] = fig_to_base64()
106
+ # 4. Platform Performance
 
 
 
 
 
107
  platforms = ["X", "Facebook", "Instagram", "PTT", "Line"]
108
  platform_counts = pd.Series({p: random.randint(10, 100) for p in platforms})
109
  fig = plt.figure(figsize=(8, 5))
110
  plt.bar(platforms, platform_counts, color='skyblue')
111
+ plt.title("Post Volume by Platform")
112
+ plt.xlabel("Platform")
113
+ plt.ylabel("Number of Posts")
114
  results["img_platform_performance"] = fig_to_base64()
115
+ # 5. Candidate Volume Trend
 
 
 
 
 
116
  candidate_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Candidate']).size().unstack(fill_value=0)
117
  fig = plt.figure(figsize=(8, 5))
118
  for c in candidates:
119
  if c in candidate_trend.columns:
120
  plt.plot(candidate_trend.index, candidate_trend[c], marker='o', label=c)
121
+ plt.title("Candidate Post Volume Trend")
122
+ plt.xlabel("Date")
123
+ plt.ylabel("Number of Posts")
124
  plt.legend()
125
  results["img_candidate_volume"] = fig_to_base64()
126
+ # 6. Candidate Sentiment Analysis
 
 
 
 
 
127
  fig = plt.figure(figsize=(8, 5))
128
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
129
  kind='bar', stacked=True, colormap='coolwarm', ax=fig.gca()
130
  )
131
+ plt.title("Candidate Sentiment Analysis (Positive/Negative/Neutral)")
132
+ plt.ylabel("Ratio")
133
+ plt.xlabel("Candidate")
134
+ plt.legend(["Positive", "Negative", "Neutral"])
135
  results["img_candidate_sentiment"] = fig_to_base64()
136
+ # 7. Knowledge Graph
137
  fig, ax = plt.subplots(figsize=(8, 6))
138
  G = nx.Graph()
139
  for c in candidates:
 
141
  for i in range(len(candidates) - 1):
142
  G.add_edge(candidates[i], candidates[i + 1])
143
  nx.draw(G, nx.spring_layout(G), with_labels=True, node_color='lightgreen', font_size=12, ax=ax)
144
+ plt.title("Candidate Knowledge Graph")
145
  results["img_knowledge_graph"] = fig_to_base64()
146
  return results
147
+ # ===== Main Analysis Function =====
148
  def run_analysis():
149
  try:
150
  # Embed the template as a string to avoid file dependency and ensure syntax is correct
 
154
  <head>
155
  <meta charset="UTF-8">
156
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
157
+ <title>2026 Kaohsiung Mayoral Election Sentiment Analysis Report</title>
158
  <script src="https://cdn.tailwindcss.com"></script>
159
  </head>
160
  <body class="bg-gray-100 font-sans leading-normal tracking-normal">
161
  <div class="container mx-auto p-4">
162
+ <h1 class="text-3xl font-bold mb-4">2026 Kaohsiung Mayoral Election Sentiment Analysis Report</h1>
163
+ <p class="mb-4">Report Date: {{ report_date }}</p>
164
 
165
+ <h2 class="text-2xl font-bold mb-2">Engagement Summary</h2>
166
  {{ engagement_table | safe }}
167
 
168
+ <h2 class="text-2xl font-bold mb-2">News Summary</h2>
169
  <ul class="list-disc pl-5 mb-4">
170
  {% for key, value in news_summary %}
171
  <li><strong>{{ key }}</strong>: {{ value }}</li>
172
  {% endfor %}
173
  </ul>
174
 
175
+ <h2 class="text-2xl font-bold mb-2">News Details</h2>
176
  {{ news_table | safe }}
177
 
178
+ <h2 class="text-2xl font-bold mb-2">Daily Sentiment Ratio</h2>
179
+ <img src="data:image/png;base64,{{ img_b64_today }}" alt="Daily Sentiment Ratio" class="mb-4">
180
 
181
+ <h2 class="text-2xl font-bold mb-2">Historical Sentiment Trend</h2>
182
+ <img src="data:image/png;base64,{{ img_b64_trend }}" alt="Historical Sentiment Trend" class="mb-4">
183
 
184
+ <h2 class="text-2xl font-bold mb-2">Social Sentiment Trend</h2>
185
+ <img src="data:image/png;base64,{{ img_social_sentiment }}" alt="Social Sentiment Trend" class="mb-4">
186
 
187
+ <h2 class="text-2xl font-bold mb-2">Platform Performance</h2>
188
+ <img src="data:image/png;base64,{{ img_platform_performance }}" alt="Platform Performance" class="mb-4">
189
 
190
+ <h2 class="text-2xl font-bold mb-2">Candidate Volume Trend</h2>
191
+ <img src="data:image/png;base64,{{ img_candidate_volume }}" alt="Candidate Volume Trend" class="mb-4">
192
 
193
+ <h2 class="text-2xl font-bold mb-2">Candidate Sentiment Analysis</h2>
194
+ <img src="data:image/png;base64,{{ img_candidate_sentiment }}" alt="Candidate Sentiment Analysis" class="mb-4">
195
 
196
+ <h2 class="text-2xl font-bold mb-2">Knowledge Graph</h2>
197
+ <img src="data:image/png;base64,{{ img_knowledge_graph }}" alt="Knowledge Graph" class="mb-4">
198
  </div>
199
  </body>
200
  </html>
201
  """
202
+ # --- Tweet & Sentiment Analysis ---
203
  all_df = pd.concat([fetch_tweets(c) for c in candidates], ignore_index=True)
204
  all_df['Sentiment'] = all_df['Content'].apply(lambda x: sentiment(x)['label'])
205
  all_df['Confidence'] = all_df['Content'].apply(lambda x: sentiment(x)['score'])
206
+ # --- Statistics ---
207
  summary = all_df.groupby(['Candidate', 'Sentiment']).size().unstack(fill_value=0)
208
  summary['Total Posts'] = summary.sum(axis=1)
209
  summary['Positive Ratio'] = summary.get('positive', 0) / summary['Total Posts'].replace(0, 1)
210
  summary['Negative Ratio'] = summary.get('negative', 0) / summary['Total Posts'].replace(0, 1)
211
  summary['Neutral Ratio'] = summary.get('neutral', 0) / summary['Total Posts'].replace(0, 1)
212
+ # --- Historical Data ---
213
  today_str = datetime.now().strftime('%Y-%m-%d')
214
  hist_row = summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].copy()
215
  hist_row['Date'] = today_str
 
219
  ignore_index=True
220
  ) if os.path.exists(history_file) else hist_row
221
  df_hist.to_csv(history_file, index=False)
222
+ # --- Charts ---
223
  charts = generate_charts(all_df, summary, df_hist)
224
+ # --- News ---
225
  if os.path.exists(news_file):
226
  df_news = pd.read_csv(news_file)
227
  news_summary = df_news.groupby('Category').size().to_dict()
228
  news_table = df_news.to_html(index=False, classes="min-w-full border border-gray-200")
229
  else:
230
  news_summary = {
231
+ "Polls": "柯志恩 leads green camp candidates in multiple polls.",
232
+ "Party Competition": "DPP primary competition is intense.",
233
+ "Controversy": "林岱樺 involved in assistant fee controversy."
234
  }
235
+ news_table = "<p>No news data available</p>"
236
  # Convert news_summary to list of tuples to support iteration in template
237
  news_summary = list(news_summary.items())
238
+ # --- Engagement Table ---
239
  engagement_table = f"""
240
  <table class="min-w-full bg-white border border-gray-200">
241
  <tr class="bg-gray-100 border-b">
242
+ <th class="py-2 px-4 border-r">Total Engagement</th>
243
  <td class="py-2 px-4 border-r">{len(all_df)}</td>
244
+ <th class="py-2 px-4 border-r">Positive Sentiment Ratio</th>
245
  <td class="py-2 px-4 border-r">{all_df['Sentiment'].value_counts(normalize=True).get('positive', 0):.1%}</td>
246
+ <th class="py-2 px-4 border-r">Average Interaction Rate</th>
247
  <td class="py-2 px-4 border-r">3.9%</td>
248
+ <th class="py-2 px-4 border-r">Active Platforms</th>
249
  <td class="py-2 px-4">{5}</td>
250
  </tr>
251
  </table>
252
  """
253
+ # --- HTML Rendering ---
254
  template = Template(html_template)
255
  html_content = template.render(
256
  report_date=datetime.now().strftime('%Y-%m-%d %H:%M'),
257
+ engagement_table=engagement_table if engagement_table else "<p>No engagement data provided</p>",
258
+ news_summary=news_summary if news_summary else "<p>No news summary provided</p>",
259
+ news_table=news_table if news_table else "<p>No news data provided</p>",
260
  **charts
261
  )
262
 
263
  return html_content
264
  except Exception:
265
+ return f"<pre>❌ Analysis Failed:\n{traceback.format_exc()}</pre>"
266
+ # ===== Gradio Frontend =====
267
  if __name__ == "__main__":
268
  iface = gr.Interface(
269
  fn=run_analysis,
270
  inputs=[],
271
  outputs=gr.HTML(),
272
+ title="2026 Kaohsiung Mayoral Election Sentiment Analysis"
273
  )
274
  iface.launch(server_name="0.0.0.0", server_port=7860)