Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1 |
-
# app.py — Multi-Modal Emotion AI (Text
|
2 |
-
#
|
3 |
-
#
|
|
|
4 |
|
5 |
-
import os,
|
6 |
from typing import Dict, List, Optional, Tuple
|
7 |
|
8 |
import numpy as np
|
@@ -15,34 +16,36 @@ import gradio as gr
|
|
15 |
from fpdf import FPDF
|
16 |
from transformers import pipeline
|
17 |
|
18 |
-
#
|
19 |
-
#
|
20 |
-
#
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
FACE_MODEL = "trpakov/vit-face-expression" # facial expression (ViT)
|
24 |
|
|
|
25 |
text_pipe = pipeline("text-classification", model=TEXT_MODEL, top_k=None)
|
26 |
voice_pipe = pipeline("audio-classification", model=VOICE_MODEL, top_k=None)
|
27 |
face_pipe = pipeline("image-classification", model=FACE_MODEL, top_k=None)
|
28 |
|
29 |
-
#
|
30 |
-
#
|
31 |
-
#
|
32 |
-
RUN_LOG = "runs.csv"
|
33 |
-
if not os.path.exists(RUN_LOG):
|
34 |
-
pd.DataFrame(columns=["timestamp","text","text_top","voice_top","face_top","fused_top","pos_index"]).to_csv(RUN_LOG, index=False)
|
35 |
-
|
36 |
-
os.makedirs("charts", exist_ok=True)
|
37 |
-
|
38 |
-
# -----------------------------
|
39 |
-
# Safety & CBT
|
40 |
-
# -----------------------------
|
41 |
RISK_TERMS = {
|
42 |
"self_harm": ["kill myself","end it","suicide","self harm","cutting","overdose"],
|
43 |
"violence": ["hurt them","attack","kill them","shoot","stab","revenge"]
|
44 |
}
|
45 |
-
|
46 |
DISTORTIONS = {
|
47 |
"catastrophizing": ["ruined","disaster","worst ever","nothing will work","everything is over"],
|
48 |
"all_or_nothing": ["always","never","completely","totally","entirely"],
|
@@ -79,13 +82,15 @@ def detect_distortions(text: str) -> List[str]:
|
|
79 |
def reframe_tips(names: List[str]) -> List[str]:
|
80 |
return [REFRAMES[n] for n in names if n in REFRAMES]
|
81 |
|
82 |
-
#
|
83 |
-
# Emotion
|
84 |
-
#
|
85 |
-
POSITIVE = set(["admiration","amusement","approval","gratitude","joy","love",
|
86 |
-
|
|
|
|
|
87 |
|
88 |
-
def
|
89 |
# pipelines return list[list[{"label","score"}]] when top_k=None
|
90 |
if isinstance(outputs, list) and outputs and isinstance(outputs[0], list):
|
91 |
outputs = outputs[0]
|
@@ -104,7 +109,7 @@ def positivity_index(prob: Optional[Dict[str,float]]) -> float:
|
|
104 |
neg = sum(prob.get(k,0.0) for k in NEGATIVE)
|
105 |
return round((pos - neg + 1)/2, 4) # [-1,1] -> [0,1]
|
106 |
|
107 |
-
def
|
108 |
labels = set()
|
109 |
for d in dicts:
|
110 |
if d: labels |= set(d.keys())
|
@@ -119,7 +124,7 @@ def union_merge(dicts: List[Optional[Dict[str,float]]], weights: List[float]) ->
|
|
119 |
def bar_fig(prob: Dict[str,float], title: str):
|
120 |
labels = list(prob.keys())
|
121 |
vals = [prob[k]*100 for k in labels]
|
122 |
-
fig, ax = plt.subplots(figsize=(7.
|
123 |
ax.bar(labels, vals)
|
124 |
ax.set_ylim(0, 100)
|
125 |
ax.set_ylabel("Probability (%)")
|
@@ -135,14 +140,14 @@ def save_chart(prob: Dict[str,float], title: str, path: str):
|
|
135 |
fig.savefig(path, dpi=160, bbox_inches="tight")
|
136 |
plt.close(fig)
|
137 |
|
138 |
-
#
|
139 |
# Computer vision: face crop
|
140 |
-
#
|
141 |
HAAR = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
|
142 |
def crop_face(image_path: str) -> Image.Image:
|
143 |
try:
|
144 |
img = cv2.imread(image_path)
|
145 |
-
if img is None:
|
146 |
return Image.open(image_path).convert("RGB")
|
147 |
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
148 |
faces = HAAR.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=5, minSize=(80,80))
|
@@ -153,13 +158,15 @@ def crop_face(image_path: str) -> Image.Image:
|
|
153 |
except Exception:
|
154 |
return Image.open(image_path).convert("RGB")
|
155 |
|
156 |
-
#
|
157 |
# Per-modality inference
|
158 |
-
#
|
159 |
def analyze_text(text: str):
|
160 |
if not text or not text.strip():
|
161 |
return gr.Error("Please enter text."), None, None
|
162 |
-
|
|
|
|
|
163 |
msg = f"**Top Text Emotion:** {top_item(probs)} | **Positivity Index:** {positivity_index(probs):.2f}"
|
164 |
fig = bar_fig(probs, "Text Emotions")
|
165 |
return msg, fig, json.dumps(probs)
|
@@ -167,7 +174,9 @@ def analyze_text(text: str):
|
|
167 |
def analyze_voice(audio_path: Optional[str]):
|
168 |
if not audio_path:
|
169 |
return "No audio provided.", None, None
|
170 |
-
|
|
|
|
|
171 |
msg = f"**Top Voice Emotion:** {top_item(probs)}"
|
172 |
fig = bar_fig(probs, "Voice Emotions")
|
173 |
return msg, fig, json.dumps(probs)
|
@@ -175,15 +184,18 @@ def analyze_voice(audio_path: Optional[str]):
|
|
175 |
def analyze_face(image_path: Optional[str]):
|
176 |
if not image_path:
|
177 |
return "No image provided.", None, None
|
178 |
-
|
179 |
-
|
|
|
|
|
|
|
180 |
msg = f"**Top Face Emotion:** {top_item(probs)}"
|
181 |
fig = bar_fig(probs, "Face Emotions")
|
182 |
return msg, fig, json.dumps(probs)
|
183 |
|
184 |
-
#
|
185 |
# PDF Report
|
186 |
-
#
|
187 |
def build_pdf(text_in: str,
|
188 |
text_prob: Optional[Dict[str,float]],
|
189 |
voice_prob: Optional[Dict[str,float]],
|
@@ -194,17 +206,18 @@ def build_pdf(text_in: str,
|
|
194 |
|
195 |
# save charts
|
196 |
paths = []
|
197 |
-
if text_prob: save_chart(text_prob, "Text Emotions", "
|
198 |
-
if voice_prob: save_chart(voice_prob, "Voice Emotions", "
|
199 |
-
if face_prob: save_chart(face_prob, "Face Emotions", "
|
200 |
-
if fused_prob: save_chart(fused_prob, "Fused Profile", "
|
201 |
|
202 |
pdf = FPDF()
|
203 |
pdf.add_page()
|
204 |
pdf.set_font("Arial", size=16)
|
205 |
-
pdf.cell(0, 10, "
|
206 |
|
207 |
pdf.set_font("Arial", size=12)
|
|
|
208 |
pdf.cell(0, 8, f"Timestamp: {datetime.datetime.now().isoformat(sep=' ', timespec='seconds')}", ln=True)
|
209 |
pdf.multi_cell(0, 8, f"Input Text: {text_in or '(none)'}")
|
210 |
pdf.ln(2)
|
@@ -226,18 +239,18 @@ def build_pdf(text_in: str,
|
|
226 |
pdf.multi_cell(0, 7, f" • {t}")
|
227 |
pdf.ln(2)
|
228 |
|
229 |
-
for
|
230 |
-
if os.path.exists(
|
231 |
-
pdf.image(
|
232 |
pdf.ln(4)
|
233 |
|
234 |
out = "emotion_report.pdf"
|
235 |
pdf.output(out)
|
236 |
return out
|
237 |
|
238 |
-
#
|
239 |
-
# Trends
|
240 |
-
#
|
241 |
def log_run(row: dict):
|
242 |
df = pd.read_csv(RUN_LOG)
|
243 |
df.loc[len(df)] = row
|
@@ -248,37 +261,35 @@ def plot_trends():
|
|
248 |
return None
|
249 |
df = pd.read_csv(RUN_LOG)
|
250 |
if df.empty: return None
|
251 |
-
df["
|
|
|
252 |
daily = df.groupby("date")["pos_index"].mean().reset_index()
|
253 |
-
fig, ax = plt.subplots(figsize=(7,3.2))
|
254 |
ax.plot(daily["date"], daily["pos_index"], marker="o")
|
255 |
ax.set_ylim(0,1)
|
256 |
-
ax.set_ylabel("Positivity Index (0
|
257 |
ax.set_title("Positivity Trend")
|
258 |
plt.xticks(rotation=25, ha="right"); plt.tight_layout()
|
259 |
return fig
|
260 |
|
261 |
-
#
|
262 |
-
# Fusion handler
|
263 |
-
#
|
264 |
def fuse_and_report(text_json, voice_json, face_json, text_raw, w_text, w_voice, w_face):
|
265 |
te = json.loads(text_json) if text_json else None
|
266 |
ve = json.loads(voice_json) if voice_json else None
|
267 |
fe = json.loads(face_json) if face_json else None
|
268 |
-
|
269 |
-
s
|
270 |
-
weights = [w/s for w in
|
271 |
-
fused =
|
272 |
|
273 |
-
# safety + CBT
|
274 |
safety_level, safety_hits = safety_screen(text_raw or "")
|
275 |
distos = detect_distortions(text_raw or "")
|
276 |
-
tips
|
277 |
|
278 |
-
# pdf
|
279 |
pdf_path = build_pdf(text_raw, te, ve, fe, fused, safety_level, safety_hits, distos, tips)
|
280 |
|
281 |
-
# log
|
282 |
pi_val = positivity_index(te)
|
283 |
log_run({
|
284 |
"timestamp": datetime.datetime.now().isoformat(sep=" ", timespec="seconds"),
|
@@ -290,26 +301,49 @@ def fuse_and_report(text_json, voice_json, face_json, text_raw, w_text, w_voice,
|
|
290 |
"pos_index": pi_val
|
291 |
})
|
292 |
|
293 |
-
msg
|
|
|
294 |
plot = bar_fig(fused, "Fused Emotional Profile") if fused else None
|
295 |
return msg, plot, pdf_path
|
296 |
|
297 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
298 |
# Gradio UI
|
299 |
-
#
|
300 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
302 |
gr.Markdown("Analyze emotions across **text, voice, and face**, detect **safety risks** and **cognitive distortions**, "
|
303 |
"tune **fusion weights**, and download a **PDF report**. Audio/image are optional.")
|
304 |
|
305 |
-
#
|
306 |
st_text_json = gr.State()
|
307 |
st_voice_json = gr.State()
|
308 |
st_face_json = gr.State()
|
309 |
st_text_raw = gr.State()
|
310 |
|
|
|
|
|
|
|
|
|
311 |
with gr.Tab("📝 Text"):
|
312 |
-
t_in
|
313 |
t_btn = gr.Button("Analyze Text", variant="primary")
|
314 |
t_msg = gr.Markdown()
|
315 |
t_plot = gr.Plot()
|
@@ -319,14 +353,15 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
319 |
t_btn.click(_t_chain, inputs=t_in, outputs=[t_msg, t_plot, st_text_json, st_text_raw])
|
320 |
|
321 |
with gr.Tab("🎤 Voice"):
|
322 |
-
a_in
|
|
|
323 |
a_btn = gr.Button("Analyze Voice", variant="primary")
|
324 |
a_msg = gr.Markdown()
|
325 |
a_plot = gr.Plot()
|
326 |
a_btn.click(analyze_voice, inputs=a_in, outputs=[a_msg, a_plot, st_voice_json])
|
327 |
|
328 |
with gr.Tab("📷 Face"):
|
329 |
-
f_in
|
330 |
f_btn = gr.Button("Analyze Face", variant="primary")
|
331 |
f_msg = gr.Markdown()
|
332 |
f_plot = gr.Plot()
|
@@ -334,9 +369,9 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
334 |
|
335 |
with gr.Tab("🧩 Fusion + Report"):
|
336 |
with gr.Row():
|
337 |
-
w_text = gr.Slider(0, 1, value=0.
|
338 |
-
w_voice = gr.Slider(0, 1, value=0.
|
339 |
-
w_face = gr.Slider(0, 1, value=0.
|
340 |
fuse_btn = gr.Button("Fuse & Generate PDF", variant="primary")
|
341 |
fuse_msg = gr.Markdown()
|
342 |
fuse_plot = gr.Plot()
|
@@ -344,20 +379,28 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
344 |
fuse_btn.click(
|
345 |
fuse_and_report,
|
346 |
inputs=[st_text_json, st_voice_json, st_face_json, st_text_raw, w_text, w_voice, w_face],
|
347 |
-
outputs=[fuse_msg, fuse_plot, fuse_pdf]
|
|
|
348 |
)
|
|
|
|
|
|
|
349 |
|
350 |
with gr.Tab("📈 Trends"):
|
351 |
tr_btn = gr.Button("Refresh Positivity Trend")
|
352 |
tr_plot = gr.Plot()
|
353 |
tr_btn.click(plot_trends, inputs=None, outputs=tr_plot)
|
354 |
|
355 |
-
with gr.Tab("
|
356 |
-
gr.Markdown(
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
|
|
|
|
|
|
|
|
361 |
|
362 |
app = demo
|
363 |
|
|
|
1 |
+
# app.py — Multi-Modal Emotion AI (Text • Voice • Face)
|
2 |
+
# College-showcase build: fusion (weighted), PDF reports, safety + CBT tips,
|
3 |
+
# trends logging, auto face-crop, version banner, demo button, API endpoint (via api_name).
|
4 |
+
# Works on Hugging Face Spaces free CPU (GPU faster, but not required).
|
5 |
|
6 |
+
import os, json, datetime
|
7 |
from typing import Dict, List, Optional, Tuple
|
8 |
|
9 |
import numpy as np
|
|
|
16 |
from fpdf import FPDF
|
17 |
from transformers import pipeline
|
18 |
|
19 |
+
# =========================
|
20 |
+
# Config / metadata
|
21 |
+
# =========================
|
22 |
+
APP_NAME = "Multi-Modal Emotion AI"
|
23 |
+
APP_VERSION = "v1.3"
|
24 |
+
RUN_LOG = "runs.csv"
|
25 |
+
CHARTS_DIR = "charts"
|
26 |
+
os.makedirs(CHARTS_DIR, exist_ok=True)
|
27 |
+
if not os.path.exists(RUN_LOG):
|
28 |
+
pd.DataFrame(columns=[
|
29 |
+
"timestamp","text","text_top","voice_top","face_top","fused_top","pos_index"
|
30 |
+
]).to_csv(RUN_LOG, index=False)
|
31 |
+
|
32 |
+
# Public models (kept light; all public)
|
33 |
+
TEXT_MODEL = "SamLowe/roberta-base-go_emotions" # text emotions (GoEmotions, 27)
|
34 |
+
VOICE_MODEL = "superb/wav2vec2-base-superb-er" # voice emotion recognition
|
35 |
FACE_MODEL = "trpakov/vit-face-expression" # facial expression (ViT)
|
36 |
|
37 |
+
# Pipelines (cached once)
|
38 |
text_pipe = pipeline("text-classification", model=TEXT_MODEL, top_k=None)
|
39 |
voice_pipe = pipeline("audio-classification", model=VOICE_MODEL, top_k=None)
|
40 |
face_pipe = pipeline("image-classification", model=FACE_MODEL, top_k=None)
|
41 |
|
42 |
+
# =========================
|
43 |
+
# Safety & CBT utilities
|
44 |
+
# =========================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
RISK_TERMS = {
|
46 |
"self_harm": ["kill myself","end it","suicide","self harm","cutting","overdose"],
|
47 |
"violence": ["hurt them","attack","kill them","shoot","stab","revenge"]
|
48 |
}
|
|
|
49 |
DISTORTIONS = {
|
50 |
"catastrophizing": ["ruined","disaster","worst ever","nothing will work","everything is over"],
|
51 |
"all_or_nothing": ["always","never","completely","totally","entirely"],
|
|
|
82 |
def reframe_tips(names: List[str]) -> List[str]:
|
83 |
return [REFRAMES[n] for n in names if n in REFRAMES]
|
84 |
|
85 |
+
# =========================
|
86 |
+
# Emotion helpers
|
87 |
+
# =========================
|
88 |
+
POSITIVE = set(["admiration","amusement","approval","gratitude","joy","love",
|
89 |
+
"optimism","relief","pride","excitement"])
|
90 |
+
NEGATIVE = set(["anger","annoyance","disappointment","disapproval","disgust",
|
91 |
+
"embarrassment","fear","grief","nervousness","remorse","sadness"])
|
92 |
|
93 |
+
def pipe_to_probs(outputs) -> Dict[str,float]:
|
94 |
# pipelines return list[list[{"label","score"}]] when top_k=None
|
95 |
if isinstance(outputs, list) and outputs and isinstance(outputs[0], list):
|
96 |
outputs = outputs[0]
|
|
|
109 |
neg = sum(prob.get(k,0.0) for k in NEGATIVE)
|
110 |
return round((pos - neg + 1)/2, 4) # [-1,1] -> [0,1]
|
111 |
|
112 |
+
def merge_probs(dicts: List[Optional[Dict[str,float]]], weights: List[float]) -> Dict[str,float]:
|
113 |
labels = set()
|
114 |
for d in dicts:
|
115 |
if d: labels |= set(d.keys())
|
|
|
124 |
def bar_fig(prob: Dict[str,float], title: str):
|
125 |
labels = list(prob.keys())
|
126 |
vals = [prob[k]*100 for k in labels]
|
127 |
+
fig, ax = plt.subplots(figsize=(7.2, 3.6))
|
128 |
ax.bar(labels, vals)
|
129 |
ax.set_ylim(0, 100)
|
130 |
ax.set_ylabel("Probability (%)")
|
|
|
140 |
fig.savefig(path, dpi=160, bbox_inches="tight")
|
141 |
plt.close(fig)
|
142 |
|
143 |
+
# =========================
|
144 |
# Computer vision: face crop
|
145 |
+
# =========================
|
146 |
HAAR = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
|
147 |
def crop_face(image_path: str) -> Image.Image:
|
148 |
try:
|
149 |
img = cv2.imread(image_path)
|
150 |
+
if img is None:
|
151 |
return Image.open(image_path).convert("RGB")
|
152 |
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
153 |
faces = HAAR.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=5, minSize=(80,80))
|
|
|
158 |
except Exception:
|
159 |
return Image.open(image_path).convert("RGB")
|
160 |
|
161 |
+
# =========================
|
162 |
# Per-modality inference
|
163 |
+
# =========================
|
164 |
def analyze_text(text: str):
|
165 |
if not text or not text.strip():
|
166 |
return gr.Error("Please enter text."), None, None
|
167 |
+
with gr.Progress() as p:
|
168 |
+
p(0.15, desc="Analyzing text…")
|
169 |
+
probs = pipe_to_probs(text_pipe(text))
|
170 |
msg = f"**Top Text Emotion:** {top_item(probs)} | **Positivity Index:** {positivity_index(probs):.2f}"
|
171 |
fig = bar_fig(probs, "Text Emotions")
|
172 |
return msg, fig, json.dumps(probs)
|
|
|
174 |
def analyze_voice(audio_path: Optional[str]):
|
175 |
if not audio_path:
|
176 |
return "No audio provided.", None, None
|
177 |
+
with gr.Progress() as p:
|
178 |
+
p(0.15, desc="Analyzing voice…")
|
179 |
+
probs = pipe_to_probs(voice_pipe(audio_path))
|
180 |
msg = f"**Top Voice Emotion:** {top_item(probs)}"
|
181 |
fig = bar_fig(probs, "Voice Emotions")
|
182 |
return msg, fig, json.dumps(probs)
|
|
|
184 |
def analyze_face(image_path: Optional[str]):
|
185 |
if not image_path:
|
186 |
return "No image provided.", None, None
|
187 |
+
with gr.Progress() as p:
|
188 |
+
p(0.15, desc="Detecting face…")
|
189 |
+
face_img = crop_face(image_path)
|
190 |
+
p(0.6, desc="Analyzing facial expression…")
|
191 |
+
probs = pipe_to_probs(face_pipe(face_img))
|
192 |
msg = f"**Top Face Emotion:** {top_item(probs)}"
|
193 |
fig = bar_fig(probs, "Face Emotions")
|
194 |
return msg, fig, json.dumps(probs)
|
195 |
|
196 |
+
# =========================
|
197 |
# PDF Report
|
198 |
+
# =========================
|
199 |
def build_pdf(text_in: str,
|
200 |
text_prob: Optional[Dict[str,float]],
|
201 |
voice_prob: Optional[Dict[str,float]],
|
|
|
206 |
|
207 |
# save charts
|
208 |
paths = []
|
209 |
+
if text_prob: save_chart(text_prob, "Text Emotions", os.path.join(CHARTS_DIR,"text.png")); paths.append(os.path.join(CHARTS_DIR,"text.png"))
|
210 |
+
if voice_prob: save_chart(voice_prob, "Voice Emotions", os.path.join(CHARTS_DIR,"voice.png")); paths.append(os.path.join(CHARTS_DIR,"voice.png"))
|
211 |
+
if face_prob: save_chart(face_prob, "Face Emotions", os.path.join(CHARTS_DIR,"face.png")); paths.append(os.path.join(CHARTS_DIR,"face.png"))
|
212 |
+
if fused_prob: save_chart(fused_prob, "Fused Profile", os.path.join(CHARTS_DIR,"fused.png")); paths.append(os.path.join(CHARTS_DIR,"fused.png"))
|
213 |
|
214 |
pdf = FPDF()
|
215 |
pdf.add_page()
|
216 |
pdf.set_font("Arial", size=16)
|
217 |
+
pdf.cell(0, 10, f"{APP_NAME} — Report", ln=True, align="C")
|
218 |
|
219 |
pdf.set_font("Arial", size=12)
|
220 |
+
pdf.cell(0, 8, f"Version: {APP_VERSION}", ln=True)
|
221 |
pdf.cell(0, 8, f"Timestamp: {datetime.datetime.now().isoformat(sep=' ', timespec='seconds')}", ln=True)
|
222 |
pdf.multi_cell(0, 8, f"Input Text: {text_in or '(none)'}")
|
223 |
pdf.ln(2)
|
|
|
239 |
pdf.multi_cell(0, 7, f" • {t}")
|
240 |
pdf.ln(2)
|
241 |
|
242 |
+
for pth in paths:
|
243 |
+
if os.path.exists(pth):
|
244 |
+
pdf.image(pth, w=180)
|
245 |
pdf.ln(4)
|
246 |
|
247 |
out = "emotion_report.pdf"
|
248 |
pdf.output(out)
|
249 |
return out
|
250 |
|
251 |
+
# =========================
|
252 |
+
# Trends logging
|
253 |
+
# =========================
|
254 |
def log_run(row: dict):
|
255 |
df = pd.read_csv(RUN_LOG)
|
256 |
df.loc[len(df)] = row
|
|
|
261 |
return None
|
262 |
df = pd.read_csv(RUN_LOG)
|
263 |
if df.empty: return None
|
264 |
+
df["timestamp"] = pd.to_datetime(df["timestamp"])
|
265 |
+
df["date"] = df["timestamp"].dt.date
|
266 |
daily = df.groupby("date")["pos_index"].mean().reset_index()
|
267 |
+
fig, ax = plt.subplots(figsize=(7.2,3.2))
|
268 |
ax.plot(daily["date"], daily["pos_index"], marker="o")
|
269 |
ax.set_ylim(0,1)
|
270 |
+
ax.set_ylabel("Positivity Index (0–1)")
|
271 |
ax.set_title("Positivity Trend")
|
272 |
plt.xticks(rotation=25, ha="right"); plt.tight_layout()
|
273 |
return fig
|
274 |
|
275 |
+
# =========================
|
276 |
+
# Fusion + API handler
|
277 |
+
# =========================
|
278 |
def fuse_and_report(text_json, voice_json, face_json, text_raw, w_text, w_voice, w_face):
|
279 |
te = json.loads(text_json) if text_json else None
|
280 |
ve = json.loads(voice_json) if voice_json else None
|
281 |
fe = json.loads(face_json) if face_json else None
|
282 |
+
ws = [w_text, w_voice, w_face]
|
283 |
+
s = sum(ws) or 1.0
|
284 |
+
weights = [w/s for w in ws]
|
285 |
+
fused = merge_probs([te, ve, fe], weights) if (te or ve or fe) else None
|
286 |
|
|
|
287 |
safety_level, safety_hits = safety_screen(text_raw or "")
|
288 |
distos = detect_distortions(text_raw or "")
|
289 |
+
tips = reframe_tips(distos)
|
290 |
|
|
|
291 |
pdf_path = build_pdf(text_raw, te, ve, fe, fused, safety_level, safety_hits, distos, tips)
|
292 |
|
|
|
293 |
pi_val = positivity_index(te)
|
294 |
log_run({
|
295 |
"timestamp": datetime.datetime.now().isoformat(sep=" ", timespec="seconds"),
|
|
|
301 |
"pos_index": pi_val
|
302 |
})
|
303 |
|
304 |
+
msg = f"**Fused Top:** {top_item(fused) or '(insufficient inputs)'}"
|
305 |
+
msg += f" | Weights → Text:{weights[0]:.2f}, Voice:{weights[1]:.2f}, Face:{weights[2]:.2f}"
|
306 |
plot = bar_fig(fused, "Fused Emotional Profile") if fused else None
|
307 |
return msg, plot, pdf_path
|
308 |
|
309 |
+
# Optional text-only JSON API (returns distribution) — exposed as /run/text_api via api_name
|
310 |
+
def text_api(text: str):
|
311 |
+
if not text or not text.strip():
|
312 |
+
return {"error":"text required"}
|
313 |
+
probs = pipe_to_probs(text_pipe(text))
|
314 |
+
return {
|
315 |
+
"top": top_item(probs),
|
316 |
+
"positivity_index": positivity_index(probs),
|
317 |
+
"distribution": probs
|
318 |
+
}
|
319 |
+
|
320 |
+
# =========================
|
321 |
# Gradio UI
|
322 |
+
# =========================
|
323 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
324 |
+
# Version banner
|
325 |
+
gr.HTML(f"""
|
326 |
+
<div style="padding:10px 12px;border:1px solid #eee;border-radius:10px;
|
327 |
+
display:flex;justify-content:space-between;align-items:center;">
|
328 |
+
<div><b>🧠 {APP_NAME}</b> — <span style="opacity:.8">Text • Voice • Face</span></div>
|
329 |
+
<div style="opacity:.7">{APP_VERSION} · MIT · Made with 🤗</div>
|
330 |
+
</div>
|
331 |
+
""")
|
332 |
gr.Markdown("Analyze emotions across **text, voice, and face**, detect **safety risks** and **cognitive distortions**, "
|
333 |
"tune **fusion weights**, and download a **PDF report**. Audio/image are optional.")
|
334 |
|
335 |
+
# State
|
336 |
st_text_json = gr.State()
|
337 |
st_voice_json = gr.State()
|
338 |
st_face_json = gr.State()
|
339 |
st_text_raw = gr.State()
|
340 |
|
341 |
+
with gr.Row():
|
342 |
+
demo_btn = gr.Button("Load demo text", variant="secondary")
|
343 |
+
reset_btn = gr.Button("Reset Weights", variant="secondary")
|
344 |
+
|
345 |
with gr.Tab("📝 Text"):
|
346 |
+
t_in = gr.Textbox(label="Your text", lines=3, placeholder="How are you feeling today?")
|
347 |
t_btn = gr.Button("Analyze Text", variant="primary")
|
348 |
t_msg = gr.Markdown()
|
349 |
t_plot = gr.Plot()
|
|
|
353 |
t_btn.click(_t_chain, inputs=t_in, outputs=[t_msg, t_plot, st_text_json, st_text_raw])
|
354 |
|
355 |
with gr.Tab("🎤 Voice"):
|
356 |
+
a_in = gr.Audio(sources=["microphone","upload"], type="filepath",
|
357 |
+
label="Record or upload audio (optional)")
|
358 |
a_btn = gr.Button("Analyze Voice", variant="primary")
|
359 |
a_msg = gr.Markdown()
|
360 |
a_plot = gr.Plot()
|
361 |
a_btn.click(analyze_voice, inputs=a_in, outputs=[a_msg, a_plot, st_voice_json])
|
362 |
|
363 |
with gr.Tab("📷 Face"):
|
364 |
+
f_in = gr.Image(type="filepath", label="Upload a face image (optional)")
|
365 |
f_btn = gr.Button("Analyze Face", variant="primary")
|
366 |
f_msg = gr.Markdown()
|
367 |
f_plot = gr.Plot()
|
|
|
369 |
|
370 |
with gr.Tab("🧩 Fusion + Report"):
|
371 |
with gr.Row():
|
372 |
+
w_text = gr.Slider(0, 1, value=0.50, step=0.05, label="Text weight")
|
373 |
+
w_voice = gr.Slider(0, 1, value=0.30, step=0.05, label="Voice weight")
|
374 |
+
w_face = gr.Slider(0, 1, value=0.20, step=0.05, label="Face weight")
|
375 |
fuse_btn = gr.Button("Fuse & Generate PDF", variant="primary")
|
376 |
fuse_msg = gr.Markdown()
|
377 |
fuse_plot = gr.Plot()
|
|
|
379 |
fuse_btn.click(
|
380 |
fuse_and_report,
|
381 |
inputs=[st_text_json, st_voice_json, st_face_json, st_text_raw, w_text, w_voice, w_face],
|
382 |
+
outputs=[fuse_msg, fuse_plot, fuse_pdf],
|
383 |
+
api_name="predict" # public API endpoint for this action
|
384 |
)
|
385 |
+
def _reset():
|
386 |
+
return 0.50, 0.30, 0.20
|
387 |
+
reset_btn.click(_reset, None, [w_text, w_voice, w_face])
|
388 |
|
389 |
with gr.Tab("📈 Trends"):
|
390 |
tr_btn = gr.Button("Refresh Positivity Trend")
|
391 |
tr_plot = gr.Plot()
|
392 |
tr_btn.click(plot_trends, inputs=None, outputs=tr_plot)
|
393 |
|
394 |
+
with gr.Tab("🔌 API"):
|
395 |
+
gr.Markdown("**Text-only JSON API** (for quick programmatic use).")
|
396 |
+
api_in = gr.Textbox(label="Text")
|
397 |
+
api_out = gr.JSON(label="Response")
|
398 |
+
gr.Button("Run API").click(text_api, inputs=api_in, outputs=api_out, api_name="text_api")
|
399 |
+
|
400 |
+
# Demo filler
|
401 |
+
def load_demo():
|
402 |
+
return "I’m stressed about deadlines but also excited for the opportunity."
|
403 |
+
demo_btn.click(load_demo, None, t_in)
|
404 |
|
405 |
app = demo
|
406 |
|