Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +42 -91
src/streamlit_app.py
CHANGED
|
@@ -2,44 +2,31 @@
|
|
| 2 |
# src/streamlit_app.py
|
| 3 |
|
| 4 |
import os
|
| 5 |
-
#
|
| 6 |
-
# Streamlit 会在 STREAMLIT_HOME 下写文件,强制指向 /app/.streamlit
|
| 7 |
os.environ["STREAMLIT_HOME"] = os.path.join(os.getcwd(), ".streamlit")
|
| 8 |
-
os.makedirs(os.environ["STREAMLIT_HOME"], exist_ok=True)
|
| 9 |
|
| 10 |
-
# DeepFace 会在 DEEPFACE_HOME 下缓存模型权重,指向 /app/.deepface
|
| 11 |
-
os.environ["DEEPFACE_HOME"] = os.path.join(os.getcwd(), ".deepface")
|
| 12 |
-
os.makedirs(os.environ["DEEPFACE_HOME"], exist_ok=True)
|
| 13 |
-
|
| 14 |
-
# ========== 2. 引入依赖 ==========
|
| 15 |
import streamlit as st
|
| 16 |
-
|
| 17 |
-
import
|
| 18 |
-
import numpy as np
|
| 19 |
-
import librosa
|
| 20 |
-
import joblib
|
| 21 |
from deepface import DeepFace
|
| 22 |
|
| 23 |
-
#
|
| 24 |
-
st.set_page_config(page_title="多模態情緒分析", layout="wide")
|
| 25 |
-
|
| 26 |
@st.cache_resource(show_spinner=False)
|
| 27 |
-
def
|
| 28 |
-
#
|
| 29 |
DeepFace.analyze(
|
| 30 |
-
img_path = np.zeros((224,224,3), dtype=
|
| 31 |
-
actions
|
| 32 |
-
enforce_detection
|
| 33 |
)
|
| 34 |
-
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
def load_audio_model():
|
| 38 |
-
# 必须保证 voice_model.joblib 在 /app 根目录
|
| 39 |
-
return joblib.load("voice_model.joblib")
|
| 40 |
|
| 41 |
-
# 文
|
| 42 |
-
def analyze_text_fn(text
|
| 43 |
if any(w in text for w in ["開心","快樂","愉快","喜悅","歡喜","興奮","歡","高興"]):
|
| 44 |
return "happy"
|
| 45 |
if any(w in text for w in ["生氣","憤怒","不爽","發火","火大","氣憤"]):
|
|
@@ -50,79 +37,43 @@ def analyze_text_fn(text:str) -> str:
|
|
| 50 |
return "surprise"
|
| 51 |
if any(w in text for w in ["怕","恐懼","緊張","懼","膽怯","畏"]):
|
| 52 |
return "fear"
|
| 53 |
-
return "neutral"
|
| 54 |
|
| 55 |
-
#
|
| 56 |
-
def analyze_audio_fn(
|
| 57 |
-
|
|
|
|
|
|
|
| 58 |
mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
return
|
| 62 |
-
|
| 63 |
-
# 预加载
|
| 64 |
-
_ = load_face_warmup()
|
| 65 |
-
audio_model = load_audio_model()
|
| 66 |
|
| 67 |
-
#
|
| 68 |
-
st.title("📱 多模態
|
| 69 |
|
| 70 |
-
tabs = st.tabs([
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
-
# ----- Tab1: 人脸实时 -----
|
| 73 |
with tabs[0]:
|
| 74 |
-
st.
|
| 75 |
-
|
| 76 |
-
def __init__(self):
|
| 77 |
-
self.last_emo = "neutral"
|
| 78 |
-
def transform(self, frame):
|
| 79 |
-
img = frame.to_ndarray(format="bgr24")
|
| 80 |
-
res = DeepFace.analyze(
|
| 81 |
-
img, actions=["emotion"], enforce_detection=False
|
| 82 |
-
)
|
| 83 |
-
# DeepFace 可能返回 list 或 dict
|
| 84 |
-
if isinstance(res, list):
|
| 85 |
-
emo = res[0].get("dominant_emotion", "unknown")
|
| 86 |
-
else:
|
| 87 |
-
emo = res.get("dominant_emotion", "unknown")
|
| 88 |
-
self.last_emo = emo
|
| 89 |
-
# 在图上标文字
|
| 90 |
-
cv2.putText(img, emo, (10,40),
|
| 91 |
-
cv2.FONT_HERSHEY_SIMPLEX, 1.2,
|
| 92 |
-
(0,255,0), 2, cv2.LINE_AA)
|
| 93 |
-
return img
|
| 94 |
-
|
| 95 |
-
ctx = webrtc_streamer(
|
| 96 |
-
key="face-emotion",
|
| 97 |
-
mode="SENDRECV",
|
| 98 |
-
media_stream_constraints={"video": True, "audio": False},
|
| 99 |
-
video_transformer_factory=FaceEmotionTransformer,
|
| 100 |
-
async_transform=True,
|
| 101 |
-
)
|
| 102 |
-
|
| 103 |
-
if ctx.video_transformer:
|
| 104 |
-
st.markdown(f"**目前檢測到情緒:** `{ctx.video_transformer.last_emo}`")
|
| 105 |
-
else:
|
| 106 |
-
st.write("請點擊下方「Start」按鈕開始攝像頭")
|
| 107 |
|
| 108 |
-
# ----- Tab2: 语音上传 -----
|
| 109 |
with tabs[1]:
|
| 110 |
-
st.
|
| 111 |
-
|
| 112 |
-
if
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
f.write(audio_file.getbuffer())
|
| 116 |
-
emo = analyze_audio_fn(tmp_path)
|
| 117 |
-
st.success(f"🎤 語音情緒預測:**{emo}**")
|
| 118 |
-
os.remove(tmp_path)
|
| 119 |
|
| 120 |
-
# ----- Tab3: 文本分析 -----
|
| 121 |
with tabs[2]:
|
| 122 |
-
st.
|
| 123 |
-
txt = st.text_area("在此輸入
|
| 124 |
-
if st.button("分析
|
| 125 |
emo = analyze_text_fn(txt)
|
| 126 |
-
st.success(f"
|
| 127 |
|
| 128 |
|
|
|
|
| 2 |
# src/streamlit_app.py
|
| 3 |
|
| 4 |
import os
|
| 5 |
+
# ① 告訴 Streamlit 將設定檔讀自專案的 .streamlit 資料夾
|
|
|
|
| 6 |
os.environ["STREAMLIT_HOME"] = os.path.join(os.getcwd(), ".streamlit")
|
|
|
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
import streamlit as st
|
| 9 |
+
import cv2, numpy as np, base64, io
|
| 10 |
+
import librosa, joblib
|
|
|
|
|
|
|
|
|
|
| 11 |
from deepface import DeepFace
|
| 12 |
|
| 13 |
+
# —— 1. 預先載入模型 ——
|
|
|
|
|
|
|
| 14 |
@st.cache_resource(show_spinner=False)
|
| 15 |
+
def load_models():
|
| 16 |
+
# a) 先讓 DeepFace 熱身(不實際偵測人臉)
|
| 17 |
DeepFace.analyze(
|
| 18 |
+
img_path = np.zeros((224,224,3), dtype=np.uint8),
|
| 19 |
+
actions = ['emotion'],
|
| 20 |
+
enforce_detection=False
|
| 21 |
)
|
| 22 |
+
# b) 載入你本機訓練好的語音模型
|
| 23 |
+
audio_model = joblib.load("voice_model.joblib")
|
| 24 |
+
return audio_model
|
| 25 |
|
| 26 |
+
audio_model = load_models()
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
+
# —— 2. 文本情緒函式 ——
|
| 29 |
+
def analyze_text_fn(text):
|
| 30 |
if any(w in text for w in ["開心","快樂","愉快","喜悅","歡喜","興奮","歡","高興"]):
|
| 31 |
return "happy"
|
| 32 |
if any(w in text for w in ["生氣","憤怒","不爽","發火","火大","氣憤"]):
|
|
|
|
| 37 |
return "surprise"
|
| 38 |
if any(w in text for w in ["怕","恐懼","緊張","懼","膽怯","畏"]):
|
| 39 |
return "fear"
|
| 40 |
+
return "neutral"
|
| 41 |
|
| 42 |
+
# —— 3. 語音情緒函式 ——
|
| 43 |
+
def analyze_audio_fn(wav_bytes):
|
| 44 |
+
# 將上傳的 bytes 用 librosa 讀入
|
| 45 |
+
y, sr = librosa.load(io.BytesIO(wav_bytes), sr=None)
|
| 46 |
+
# 計算 MFCC 並取均值作為特徵
|
| 47 |
mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
|
| 48 |
+
mf = np.mean(mfccs.T, axis=0)
|
| 49 |
+
# 回傳模型預測結果
|
| 50 |
+
return audio_model.predict([mf])[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
# —— 4. 網頁介面佈局 ——
|
| 53 |
+
st.title("📱 多模態即時情緒分析")
|
| 54 |
|
| 55 |
+
tabs = st.tabs([
|
| 56 |
+
"🔴 臉部(僅限本地)",
|
| 57 |
+
"🎤 語音上傳",
|
| 58 |
+
"⌨️ 文字輸入"
|
| 59 |
+
])
|
| 60 |
|
|
|
|
| 61 |
with tabs[0]:
|
| 62 |
+
st.header("實時臉部(本地瀏覽器測試用)")
|
| 63 |
+
st.info("⚠️ HF Spaces 無法直接存取攝影機,僅本地測試有效。")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
|
|
|
| 65 |
with tabs[1]:
|
| 66 |
+
st.header("上傳 WAV 檔案進行分析")
|
| 67 |
+
wav_file = st.file_uploader("選擇一個 .wav 音訊檔", type="wav")
|
| 68 |
+
if wav_file:
|
| 69 |
+
emo = analyze_audio_fn(wav_file.read())
|
| 70 |
+
st.success(f"🎤 語音檢測到的情緒:**{emo}**")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
|
|
|
| 72 |
with tabs[2]:
|
| 73 |
+
st.header("輸入文字進行分析")
|
| 74 |
+
txt = st.text_area("在此輸入或貼上文字")
|
| 75 |
+
if st.button("開始分析"):
|
| 76 |
emo = analyze_text_fn(txt)
|
| 77 |
+
st.success(f"📝 文本檢測到的情緒:**{emo}**")
|
| 78 |
|
| 79 |
|