File size: 7,267 Bytes
b8ff14e
283d228
c7ec63e
7b9bebe
c7ec63e
283d228
c7ec63e
287ab51
283d228
 
 
 
 
 
 
287ab51
283d228
c7ec63e
 
 
 
 
283d228
 
 
 
 
 
 
 
2a89f5d
 
283d228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36c201f
 
 
 
 
2a89f5d
283d228
36c201f
 
7b9bebe
283d228
 
7b9bebe
 
2a89f5d
283d228
 
7b9bebe
 
283d228
 
7b9bebe
283d228
7b9bebe
283d228
 
 
 
 
dfd5bb6
36c201f
283d228
 
4eafa91
36c201f
 
283d228
2a89f5d
283d228
 
2a89f5d
283d228
 
2a89f5d
283d228
 
36c201f
283d228
7b9bebe
 
 
dfd5bb6
7b9bebe
 
 
 
 
 
 
 
 
 
 
 
c7ec63e
283d228
c0131be
 
7b9bebe
c0131be
 
7b9bebe
c0131be
 
7b9bebe
287ab51
 
 
 
c0131be
 
 
 
 
 
 
 
 
 
 
287ab51
 
 
c0131be
287ab51
 
283d228
82dd2b7
c0131be
287ab51
c0131be
b9af6a3
 
 
c0131be
287ab51
c0131be
287ab51
7b9bebe
c0131be
 
c7ec63e
b9af6a3
283d228
b1ced09
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import gradio as gr
import os
import numpy as np
import joblib
import librosa
import requests
from huggingface_hub import hf_hub_download

# --- DeepFace 条件导入 ---
try:
    from deepface import DeepFace
    has_deepface = True
except ImportError:
    print("本地未安装 deepface,将在本地跳过臉部情緒;Space 上会安装 deepface。")
    has_deepface = False

# --- 1. 语音 SVM 加载 ---
print("Downloading SVM model from Hugging Face Hub...")
model_path = hf_hub_download(repo_id="GCLing/emotion-svm-model", filename="svm_emotion_model.joblib")
svm_model = joblib.load(model_path)
print("SVM model loaded.")

# --- 2. 文本情绪分析:改用 Inference API ---
HF_API_TOKEN = os.getenv("HF_API_TOKEN")
if HF_API_TOKEN is None:
    print("警告:未检测到 HF_API_TOKEN,Inference API 可能失败。")
# 选用公开存在的中文情感分类模型
HF_TEXT_MODEL = "uer/roberta-base-finetuned-dianping-chinese"
HF_API_URL = f"https://api-inference.huggingface.co/models/{HF_TEXT_MODEL}"
headers = {"Authorization": f"Bearer {HF_API_TOKEN}"} if HF_API_TOKEN else {}


def predict_text_via_api(text: str):
    if not text or text.strip()=="":
        return {}
    payload = {"inputs": text}
    try:
        resp = requests.post(HF_API_URL, headers=headers, json=payload, timeout=30)
        if resp.status_code != 200:
            print(f"Inference API 返回状态码 {resp.status_code}: {resp.text}")
            # 退回到简单规则或中性
            return {"中性": 1.0}
        data = resp.json()
        # 根据模型返回格式解析:假设返回 [{"label": "...", "score": ...}, ...]
        if isinstance(data, list) and len(data)>0 and isinstance(data[0], dict):
            # 选 top 3 展示
            result = {}
            for item in data[:3]:
                lbl = item.get("label", "")
                score = item.get("score", 0.0)
                # 若标签是英文,可映射到中文;若就是中文可直接用
                # 例如模型返回 "positive"/"negative"/"neutral",可映射:
                if lbl.lower() in ["positive","pos","正面"]:
                    cn = "正面"
                elif lbl.lower() in ["negative","neg","负面","負面"]:
                    cn = "負面"
                elif lbl.lower() in ["neutral","中性"]:
                    cn = "中性"
                else:
                    cn = lbl
                result[cn] = float(score)
            return result
        else:
            print("Inference API 返回格式异常:", data)
            return {"中性": 1.0}
    except Exception as e:
        print("调用 Inference API 出错:", e)
        return {"中性": 1.0}

# 可保留简单规则优先,若规则命中则返回规则,否则调用 API
emo_keywords = {
    "happy": ["開心","快樂","愉快","喜悦","喜悅","歡喜","興奮","高興"],
    "angry": ["生氣","憤怒","不爽","發火","火大","氣憤"],
    "sad": ["傷心","難過","哭","難受","心酸","憂","悲","哀","痛苦","慘","愁"],
    "surprise": ["驚訝","意外","嚇","驚詫","詫異","訝異","好奇"],
    "fear": ["怕","恐懼","緊張","懼","膽怯","畏"],
    "disgust": ["噁心","厭惡","反感"]
}
negations = ["不","沒","沒有","別","勿","非"]
def keyword_emotion(text: str):
    text_proc = text.strip()
    counts = {emo:0 for emo in emo_keywords}
    for emo, kws in emo_keywords.items():
        for w in kws:
            idx = text_proc.find(w)
            if idx!=-1:
                neg=False
                for neg_word in negations:
                    plen = len(neg_word)
                    if idx-plen>=0 and text_proc[idx-plen:idx]==neg_word:
                        neg=True; break
                if not neg:
                    counts[emo]+=1
    total = sum(counts.values())
    if total>0:
        # 归一化并取最高
        top = max(counts, key=lambda k: counts[k])
        return {top: counts[top]/total}
    return None

def predict_text_mixed(text: str):
    print("predict_text_mixed:", text)
    if not text or text.strip()=="":
        return {}
    res = keyword_emotion(text)
    if res:
        # 映射中文标签
        mapping = {
            "happy":"高興","angry":"憤怒","sad":"悲傷",
            "surprise":"驚訝","fear":"恐懼","disgust":"厭惡"
        }
        emo = list(res.keys())[0]; prob = float(res[emo])
        cn = mapping.get(emo, emo)
        return {cn: prob}
    # 规则未命中,调用 Inference API
    return predict_text_via_api(text)

# --- 3. 语音情绪预测 ---
def extract_feature(signal: np.ndarray, sr: int) -> np.ndarray:
    mfcc = librosa.feature.mfcc(y=signal, sr=sr, n_mfcc=13)
    return np.concatenate([np.mean(mfcc, axis=1), np.var(mfcc, axis=1)])

def predict_voice(audio_path: str):
    if not audio_path:
        return {}
    try:
        signal, sr = librosa.load(audio_path, sr=None)
        feat = extract_feature(signal, sr)
        probs = svm_model.predict_proba([feat])[0]
        labels = svm_model.classes_
        return {labels[i]: float(probs[i]) for i in range(len(labels))}
    except Exception as e:
        print("predict_voice error:", e)
        return {}

# --- 4. 人脸情绪预测 ---
import gradio as gr

def predict_face(img: np.ndarray):
    # 你的 DeepFace 分析逻辑
    if img is None:
        return {}
    # ...
    return {"happy": 0.5, "sad": 0.5}  # 举例

def build_interface():
    with gr.Blocks() as demo:
        gr.Markdown("## 多模態情緒分析示例")
        with gr.Tabs():
            # 臉部情緒 Tab
            with gr.TabItem("臉部情緒"):
                gr.Markdown("### 臉部情緒 (即時 Webcam Streaming 分析)")
                with gr.Row():
                    # 这里用 gr.Image(sources="webcam", streaming=True, type="numpy")
                    webcam = gr.Image(sources="webcam", streaming=True, type="numpy", label="攝像頭畫面")
                    face_out = gr.Label(label="情緒分佈")
                # 每帧送到 predict_face
                webcam.stream(fn=predict_face, inputs=webcam, outputs=face_out)

            # 語音情緒 Tab
            with gr.TabItem("語音情緒"):
                gr.Markdown("### 語音情緒 分析")
                with gr.Row():
                    # 浏览器录音用 source="microphone"
                    audio = gr.Audio(source="microphone", streaming=False, type="filepath", label="錄音")
                    voice_out = gr.Label(label="語音情緒結果")
                audio.change(fn=predict_voice, inputs=audio, outputs=voice_out)

            # 文字情緒 Tab
            with gr.TabItem("文字情緒"):
                gr.Markdown("### 文字情緒 分析 (規則+Inference API)")
                with gr.Row():
                    text = gr.Textbox(lines=3, placeholder="請輸入中文文字…")
                    text_out = gr.Label(label="文字情緒結果")
                # 使用 submit 触发
                text.submit(fn=predict_text_mixed, inputs=text, outputs=text_out)

        return demo



if __name__ == "__main__":
    demo = build_interface()
    # share=True 可在本地测试时生成临时公网链接
    demo.launch(share=True)