lauren-cw commited on
Commit
1ae6917
·
verified ·
1 Parent(s): 75b775e

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +435 -0
app.py ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """DASS心理模型(Q12).ipynb
3
+
4
+ Automatically generated by Colab.
5
+
6
+ Original file is located at
7
+ https://colab.research.google.com/drive/19ATyW5Lb692QV2Gk2I0rlsbeSQNwEDnQ
8
+
9
+ 建立環境
10
+ """
11
+
12
+ # !pip install gradio
13
+
14
+ import pandas as pd
15
+ import matplotlib.pyplot as plt
16
+ import numpy as np
17
+ import gradio as gr
18
+ import sklearn
19
+ import pickle
20
+ import joblib
21
+ import time
22
+ import os
23
+ import json
24
+ import gspread
25
+
26
+
27
+ from sklearn.model_selection import train_test_split, RandomizedSearchCV
28
+ from sklearn.compose import ColumnTransformer
29
+ from sklearn.preprocessing import OneHotEncoder, StandardScaler
30
+ from sklearn.pipeline import Pipeline
31
+ from sklearn.linear_model import LogisticRegression
32
+ from sklearn.metrics import classification_report, confusion_matrix, f1_score, accuracy_score, precision_score, balanced_accuracy_score
33
+ from sklearn.ensemble import RandomForestClassifier
34
+ from lightgbm import LGBMClassifier
35
+ from AutoPreprocess import AutoPreprocess
36
+ from google.oauth2.service_account import Credentials
37
+ from datetime import datetime, timezone, timedelta
38
+
39
+ """Gradio 使用者介面"""
40
+
41
+ # 載入模型
42
+ import pickle
43
+ model_path = os.path.abspath("DASS_model.bin")
44
+
45
+ with open(model_path, "rb") as f:
46
+ model = pickle.load(f)
47
+ model
48
+
49
+ """定義歷史紀錄功能"""
50
+
51
+ def update_history(current_result_1, current_result_2, out_error, history_list):
52
+ """
53
+ current_result_1 & 2: 來自 predict_risk 的兩個回傳值 (HTML 字串)
54
+ history_list: 來自 gr.State 的現有紀錄列表
55
+ """
56
+ tw_timezone = timezone(timedelta(hours=8))
57
+ # 獲取當前時間,格式為:2023-10-27 14:30:05
58
+ now = datetime.now(tw_timezone).strftime("%Y-%m-%d %H:%M:%S")
59
+
60
+ # 組合這次的結果 (假設你想存這兩個 outputs 的組合)
61
+ new_entry = f"""
62
+ <div style="border-bottom: 2px solid #eee; padding-bottom: 20px; margin-bottom: 20px;">
63
+ <div style="font-size: 18px; color: #666; margin-bottom: 10px; font-weight: bold;">
64
+ 🕒 測驗時間:{now}
65
+ </div>
66
+
67
+ <div style="
68
+ display: flex;
69
+ flex-direction: row;
70
+ justify-content: space-between;
71
+ align-items: flex-start;
72
+ gap: 20px;
73
+ border-bottom: 1px dashed #ccc;
74
+ padding-bottom: 15px;
75
+ margin-bottom: 15px;
76
+ width: 100%;">
77
+
78
+ <div style="flex: 1;">{current_result_1}</div>
79
+ <div style="flex: 1;">{current_result_2}</div>
80
+
81
+ </div>
82
+ </div>
83
+ """
84
+
85
+ # 將新紀錄放在最前面 (置頂)
86
+ history_list.insert(0, new_entry)
87
+
88
+ # 組合所有歷史紀錄,並整體縮小 80%
89
+ # 使用 zoom: 0.8 或 transform 達到字體與版面同時縮小的效果
90
+ combined_html = f"""
91
+ <div style="zoom: 0.8; -moz-transform: scale(0.8); -moz-transform-origin: 0 0;">
92
+ {"".join(history_list)}
93
+ </div>
94
+ """
95
+
96
+ return combined_html, history_list
97
+
98
+ """定義儲存測試資料的功能
99
+
100
+ Google Sheet 連線
101
+ """
102
+
103
+ import os
104
+ import json
105
+ from datetime import datetime, timezone, timedelta
106
+ import gspread
107
+ from google.oauth2.service_account import Credentials
108
+
109
+ # 1. 設定 Google Sheets 存取權限
110
+ scope = ['https://www.googleapis.com/auth/spreadsheets',
111
+ 'https://www.googleapis.com/auth/drive']
112
+
113
+ # ... 之前的 import ...
114
+
115
+ sheet = None # 預設
116
+
117
+ # 寫一個連線函式
118
+ def init_gspread():
119
+ global sheet
120
+ google_json = os.environ.get("DASS_JSON")
121
+ if not google_json:
122
+ print("⚠️ 找不到 DASS_JSON")
123
+ return
124
+ try:
125
+ info = json.loads(google_json)
126
+ creds = Credentials.from_service_account_info(info, scopes=scope)
127
+ client = gspread.authorize(creds)
128
+ # 再次確認試算表名稱是否完全正確
129
+ sheet = client.open_by_key("1SPMKe5uOK7EMukB4udyEFKCibpJRNGCQdOQEUVJAgN4").sheet1
130
+ print("✅ Google Sheets 初始化成功")
131
+ except Exception as e:
132
+ print(f"❌ 初始化失敗: {e}")
133
+
134
+ def save_to_google_sheets(inputs, a_score, d_score, s_score, t_score, score):
135
+
136
+ global sheet
137
+
138
+ if sheet is None:
139
+ print("⚠️ 試算表未連線,跳過儲存步驟")
140
+ return
141
+
142
+ try:
143
+
144
+ # 設定台灣時區
145
+ tw_timezone = timezone(timedelta(hours=8))
146
+ # 1. 拆分資料:前 3 個是基本資料,後面剩下的 (*rest) 是 12 題答案
147
+ user_info = inputs[:3] # 取得前三個:姓名, 年齡, 性別
148
+ q_answers = inputs[3:] # 取得剩下的 12 題
149
+ now = datetime.now(tw_timezone).strftime("%Y-%m-%d %H:%M:%S")
150
+
151
+ # 2. 準備要儲存的資料字典
152
+ row_to_add = [
153
+ now, # 欄位 A: 測試時間
154
+ user_info[0], # 欄位 B: 性別
155
+ user_info[1], # 欄位 C: 年齡
156
+ user_info[2], # 欄位 D: 家庭人數
157
+ a_score, # 欄位 E: 焦慮分數
158
+ d_score, # 欄位 F: 憂鬱分數
159
+ s_score, # 欄位 G: 壓力分數
160
+ t_score, # 欄位 H: 總體分數
161
+ score # 欄位 I: 整體程度 (標籤)
162
+ ]
163
+
164
+ row_to_add.extend(q_answers) # 加入 Q1-Q12 (J欄以後)
165
+
166
+ # 4. 追加到試算表最後一行
167
+ sheet.append_row(row_to_add)
168
+ print(f"✨ 資料已成功存入試算表: {now}")
169
+
170
+ except Exception as e:
171
+ print(f"❌ 寫入資料時發生錯誤: {e}")
172
+
173
+ """定義重新測驗功能"""
174
+
175
+ # 清空函數:回傳與輸入組件數量相同的 None (15個:gen, age, family + 12個問題)
176
+ def clear_all():
177
+ # 15個輸入(gen, age, family, q1~q12) + 2個即時結果
178
+ return [None] * 15 + ["", ""]
179
+
180
+ """定義主要測試功能"""
181
+
182
+ def predict_risk(gen, age, family, q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12):
183
+ inputs = [gen, age, family, q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12]
184
+ result_score = ""
185
+ label_html = ""
186
+ error_message = ""
187
+
188
+ # 檢查是否有任何一個選項是 None (未填)
189
+ if any(v is None or v == "" for v in inputs):
190
+ error_message = '<div style="color: red; font-weight: bold;">⚠️測驗載入有誤:請確保每一題都已填答或查看填答格式是否正確。</div>'
191
+ return result_score, label_html, error_message
192
+
193
+ try:
194
+ # 1. 跑進度條 (需確保函式參數有 progress=gr.Progress())
195
+ progress = gr.Progress()
196
+ progress(0, desc="模型計算中...")
197
+
198
+ # 2. 將 12 個輸入整理成模型認得的 DataFrame
199
+ # 欄位名稱必須與訓練時完全相同
200
+ cols = ["gender", "age", "familysize", "Q2A", "Q4A", "Q19A", "Q20A", "Q28A", "Q21A", "Q26A", "Q37A", "Q42A", "Q11A", "Q12A", "Q27A"]
201
+ input_df = pd.DataFrame([inputs], columns=cols)
202
+
203
+ progress(0.5, desc="正在分析數據...")
204
+ time.sleep(0.5) # 模擬運算時間
205
+
206
+ # 3. 使用模型 model 進行預測
207
+ score = model.predict(input_df)[0]
208
+
209
+ # 4. 定義風險標籤
210
+ risk_map = {
211
+ 0: ("低度風險", "#91cd92"),
212
+ 1: ("中度風險", "#f59e0b"),
213
+ 2: ("高度風險", "#ef4444")
214
+ }
215
+ label, color = risk_map.get(score, ("計算結果有誤,請重新測試。", "#ef4444"))
216
+
217
+
218
+ # 定義類別分數條
219
+ a_score = sum([q1, q2, q3, q4, q5])
220
+ d_score = sum([q6, q7, q8, q9])
221
+ s_score = sum([q10, q11, q12])
222
+ t_score = a_score + d_score + s_score
223
+ max_val = 36
224
+
225
+ def make_bar(label, score, max_val, color):
226
+ percent = (score / max_val) * 100
227
+ return f"""
228
+ <div style="margin-bottom: 10px;">
229
+ <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
230
+ <span style="font-weight: bold;">{label}</span>
231
+ </div>
232
+ <div style="background-color: #e0e0e0; border-radius: 10px; height: 12px; width: 100%;">
233
+ <div style="background-color: {color}; width: {percent}%; height: 100%; border-radius: 10px;"></div>
234
+ </div>
235
+ </div>
236
+ """
237
+
238
+ # 5. 準備回傳內容
239
+ # 總分與風險標籤
240
+ result_score = f"""
241
+ <div style="text-align: center; font-family: sans-serif;">
242
+ <h2 style="color: #313230;">您的預測結果為</h2>
243
+ <h1 style="font-size: 60px; color: {color}; margin: 0;">
244
+ {label}
245
+ </h1>
246
+ <h1 style="font-size: 20px; color: #bbbbc2; margin: 0;">
247
+ {t_score}/36
248
+ </h1>
249
+ </div>
250
+ """
251
+
252
+ # 類別分數條
253
+ label_html = f"""
254
+ <div style="padding: 20px; background: white; border-radius: 10px; border: 1px solid #ddd;">
255
+ <h2 style="color: #313230;margin-top: 0; margin-bottom: 15px;">各面向之比重</h2>
256
+ {make_bar("焦慮 (Anxiety)", a_score, max_val, "#fccb42")}
257
+ {make_bar("憂鬱 (Depression)", d_score, max_val, "#6dc8fe")}
258
+ {make_bar("壓力 (Stress)", s_score, max_val, "#fb6d6d")}
259
+ </div>
260
+ """
261
+
262
+ # 儲存測試資料
263
+ try:
264
+ save_to_google_sheets(inputs, a_score, d_score, s_score, t_score, score)
265
+ except Exception as sheet_err:
266
+ print(f"Sheet Error: {sheet_err}")
267
+
268
+
269
+ progress(1.0, desc="計算完成!")
270
+ return result_score, label_html, error_message
271
+
272
+ except Exception as e:
273
+ error_message = f"<div style='color: red;'>系統錯誤: {str(e)}</div>"
274
+ return "", "", error_message
275
+
276
+ # 設定主題色
277
+
278
+ theme = gr.themes.Default(
279
+ primary_hue="amber",
280
+ secondary_hue="amber",
281
+ ).set(
282
+ body_background_fill="#fffbeb"
283
+ )
284
+
285
+ # 線上主題調色器
286
+ # gr.themes.builder()
287
+
288
+ # 執行連線
289
+ init_gspread()
290
+
291
+ # 介面編排
292
+
293
+ #按鈕及面板格式設定
294
+ custom_css = """
295
+ #my_green_btn {
296
+ background-color: #91cd92 !important;
297
+ color: white !important;
298
+ border: none;
299
+ }
300
+ #my_green_btn:hover {
301
+ background-color: #72a473 !important; /* 滑鼠懸���時變深 */
302
+ }
303
+
304
+ #my_white_btn {
305
+ background-color: #ffffff !important;
306
+ color: black !important;
307
+ border: 1px solid #e4e4e7;
308
+ }
309
+
310
+ #my_white_btn:hover {
311
+ background-color: #e4e4e7 !important;
312
+ color: black !important; /* 滑鼠懸停時變深 */
313
+ }
314
+
315
+
316
+ .my-custom-panel {
317
+ background-color: #fffef8 !important;
318
+ border: 2px solid #e4e4e7 !important;
319
+ padding: 20px;
320
+ border-radius: 15px;
321
+ }
322
+
323
+ #history_panel .label-wrap span {
324
+ font-weight: bold !important;
325
+ }
326
+ """
327
+
328
+ with gr.Blocks(theme=theme, css=custom_css) as demo:
329
+
330
+ # 建立 Session 狀態 (開啟瀏覽器時初始化為空列表)
331
+ history_state = gr.State([])
332
+
333
+ # 標題及說明
334
+ gr.Markdown("")
335
+ gr.HTML(f"""
336
+ <div style="text-align: center; font-family: sans-serif;">
337
+ <h2 style="font-size: 32px; color: #313230; margin: 0;">🌿心理健康風險程度測試📝</h2>
338
+ </div>
339
+ """)
340
+ gr.HTML(f"""
341
+ <div style="text-align: center; font-family: sans-serif;">
342
+ <h2 style="font-size: 18px; color: #313230; margin: 0;">歡迎來到心理健康風險程度測試環境!<br>
343
+ 本測驗將透過12題問答,替您在5分鐘內簡單計算出潛在的心理健康風險程度。<br>
344
+ 請輕鬆填答,無須思慮過度,測驗愉快!</h2>
345
+ </div>
346
+ """)
347
+
348
+ with gr.Column(variant="panel", elem_classes="my-custom-panel"):
349
+
350
+ # 輸入區塊1(人口靜態欄位)
351
+ gr.Markdown("## Step 1. 請輸入基本資訊")
352
+ with gr.Row():
353
+ with gr.Column():
354
+ name = gr.Textbox(label="暱稱")
355
+ gen = gr.Dropdown(choices=["男", "女", "其他"],
356
+ label="性別",
357
+ value=[])
358
+ with gr.Column():
359
+ age = gr.Number(label="年齡 (僅限填寫數字)", value ="")
360
+ family = gr.Number(label="家庭人數 (僅限填寫數字)", value ="")
361
+
362
+
363
+ # 輸入區塊2(測驗題)
364
+ gr.Markdown("")
365
+ gr.Markdown("## Step 2. 請依自身狀態選擇符合的答案")
366
+ q1 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
367
+ label="Q1.我感覺到口乾舌燥。")
368
+ q2 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
369
+ label="Q2.我感到呼吸困難(例如:在沒有體力勞動的情況下,呼吸過度急促或喘不過氣)。")
370
+ q3 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
371
+ label="Q3.在氣溫不高或沒有體力勞動的情況下,我明顯地流汗(例如:手汗)。")
372
+ q4 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
373
+ label="Q4.我無緣無故地感到害怕。")
374
+ q5 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
375
+ label="Q5.我覺得自己接近恐慌發作的邊緣。")
376
+ q6 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
377
+ label="Q6.我覺得生命沒什麼意義/價值。")
378
+ q7 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
379
+ label="Q7.我感到垂頭喪氣、情緒低落。")
380
+ q8 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
381
+ label="Q8.我覺得未來毫無希望。")
382
+ q9 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
383
+ label="Q9.我發現自己很難打起精神主動去做事。")
384
+ q10 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
385
+ label="Q10.我發現自己很容易變得心煩意亂。")
386
+ q11 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
387
+ label="Q11.我覺得自己消耗了大量的神經能量(處於高度緊繃狀態)。")
388
+ q12 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
389
+ label="Q12.我發現自己非常易怒(容易焦躁)。")
390
+
391
+ # 錯誤訊息顯示區 (放在按鈕上方)
392
+ out_error = gr.HTML()
393
+
394
+ # 確認送出、重新測驗按鈕
395
+ sub_button = gr.Button("確認送出", elem_id="my_green_btn")
396
+ btn_reset = gr.Button("重新測驗", elem_id="my_white_btn")
397
+
398
+ # 輸出測試結果
399
+ with gr.Row():
400
+ out_html = gr.HTML()
401
+ out_label = gr.HTML()
402
+
403
+
404
+ # --- 新增:歷史紀錄呈現區域 ---
405
+ with gr.Accordion("查看歷史紀錄", open=False, elem_id="history_panel"):
406
+ history_display = gr.HTML(value="目前尚無測驗紀錄")
407
+
408
+
409
+ # 按鈕設定
410
+ # 1. 確認送出
411
+ sub_button.click(
412
+ fn=predict_risk,
413
+ inputs= [gen, age, family, q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12],
414
+ outputs= [out_html, out_label, out_error]
415
+ ).then(
416
+ fn=update_history,
417
+ inputs=[out_html, out_label, out_error, history_state],
418
+ outputs=[history_display, history_state]
419
+ )
420
+
421
+ # 2. 重新測驗 (清空所有輸入與輸出)
422
+ # 注意:outputs 必須包含所有輸入的組件
423
+ btn_reset.click(
424
+ fn=lambda: [None]*15 + ["", "", ""],
425
+ inputs=None,
426
+ outputs=[gen, age, family, q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, out_html , out_label, out_error]
427
+ )
428
+
429
+ gr.Markdown("## 免責聲明")
430
+ gr.Markdown("""本測驗結果僅供參考,非屬正規醫療檢驗範疇。
431
+ 若對於自身狀況有任何疑慮,敬請尋求正規專業醫療協助!♡第四組關心您♡""")
432
+
433
+ demo.launch(share=True)
434
+
435
+ # 如需免費永久托管,需在終端機模式執行「gradio deploy」部署到 Hugging Face Spaces。