Spaces:
Sleeping
Sleeping
Upload 7 files
Browse files- .gitattributes +1 -0
- Dockerfile +26 -0
- README.md +9 -11
- frontend.py +162 -0
- logo.png +3 -0
- main.py +249 -0
- model.py +121 -0
- requirements.txt +10 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
logo.png filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ---- BASE PYTHON IMAGE ----
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# ---- ENV & WORKDIR ----
|
| 5 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 6 |
+
ENV PYTHONUNBUFFERED=1
|
| 7 |
+
WORKDIR /code
|
| 8 |
+
|
| 9 |
+
# ---- SYSTEM DEPENDENCIES ----
|
| 10 |
+
RUN apt-get update && apt-get install -y \
|
| 11 |
+
libsndfile1 ffmpeg git \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
# ---- COPY PROJECT FILES ----
|
| 15 |
+
COPY . /code
|
| 16 |
+
|
| 17 |
+
# ---- INSTALL DEPENDENCIES ----
|
| 18 |
+
RUN pip install --upgrade pip
|
| 19 |
+
RUN pip install -r requirements.txt
|
| 20 |
+
|
| 21 |
+
# ---- EXPOSE PORTS ----
|
| 22 |
+
EXPOSE 7860
|
| 23 |
+
EXPOSE 8000
|
| 24 |
+
|
| 25 |
+
# ---- LAUNCH BOTH BACKEND & FRONTEND ----
|
| 26 |
+
CMD ["bash", "-c", "uvicorn app.main:app --host 0.0.0.0 --port 8000 & streamlit run frontend.py --server.port 7860 --server.address 0.0.0.0"]
|
README.md
CHANGED
|
@@ -1,11 +1,9 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
+
# NeuroPulse AI
|
| 2 |
+
|
| 3 |
+
Multimodal Feedback Analyzer with Streamlit + FastAPI.
|
| 4 |
+
Summarization Β· Sentiment Β· Emotion Β· Aspects Β· Smart Clustering.
|
| 5 |
+
|
| 6 |
+
## Running locally
|
| 7 |
+
```bash
|
| 8 |
+
streamlit run app/frontend/frontend.py
|
| 9 |
+
python -m uvicorn app.main:app --reload
|
|
|
|
|
|
frontend.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import requests
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from gtts import gTTS
|
| 5 |
+
import base64
|
| 6 |
+
from io import BytesIO
|
| 7 |
+
from PIL import Image
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
st.set_page_config(page_title="NeuroPulse AI", page_icon="π§ ", layout="wide")
|
| 11 |
+
|
| 12 |
+
logo_path = os.path.join("app", "static", "logo.png")
|
| 13 |
+
if os.path.exists(logo_path):
|
| 14 |
+
st.image(logo_path, width=160)
|
| 15 |
+
|
| 16 |
+
# Session state
|
| 17 |
+
if "history" not in st.session_state:
|
| 18 |
+
st.session_state.history = []
|
| 19 |
+
if "dark_mode" not in st.session_state:
|
| 20 |
+
st.session_state.dark_mode = False
|
| 21 |
+
|
| 22 |
+
# Sidebar
|
| 23 |
+
with st.sidebar:
|
| 24 |
+
st.header("βοΈ Settings")
|
| 25 |
+
st.session_state.dark_mode = st.toggle("π Dark Mode", value=st.session_state.dark_mode)
|
| 26 |
+
|
| 27 |
+
sentiment_model = st.selectbox("π Sentiment Model", [
|
| 28 |
+
"distilbert-base-uncased-finetuned-sst-2-english",
|
| 29 |
+
"nlptown/bert-base-multilingual-uncased-sentiment"
|
| 30 |
+
])
|
| 31 |
+
|
| 32 |
+
industry = st.selectbox("π Industry Context", [
|
| 33 |
+
"Generic", "E-commerce", "Healthcare", "Education", "Travel", "Banking", "Insurance"
|
| 34 |
+
])
|
| 35 |
+
|
| 36 |
+
product_category = st.selectbox("π§© Product Category", [
|
| 37 |
+
"General", "Mobile Devices", "Laptops", "Healthcare Devices", "Banking App",
|
| 38 |
+
"Travel Service", "Educational Tool", "Insurance Portal"
|
| 39 |
+
])
|
| 40 |
+
|
| 41 |
+
device_type = st.selectbox("π» Device Type", [
|
| 42 |
+
"Web", "Android", "iOS", "Desktop", "Smartwatch", "Kiosk"
|
| 43 |
+
])
|
| 44 |
+
|
| 45 |
+
use_aspects = st.checkbox("π Enable Aspect-Based Analysis")
|
| 46 |
+
use_smart_summary = st.checkbox("π§ Use Smart Summary (clustered key points)")
|
| 47 |
+
use_smart_summary_bulk = st.checkbox("π§ Smart Summary for Bulk CSV")
|
| 48 |
+
|
| 49 |
+
follow_up = st.text_input("π Follow-up Question")
|
| 50 |
+
voice_lang = st.selectbox("π Voice Language", ["en", "fr", "es", "de", "hi", "zh"])
|
| 51 |
+
backend_url = st.text_input("π₯οΈ Backend URL", value="http://127.0.0.1:8000")
|
| 52 |
+
api_token = st.text_input("π API Token", type="password")
|
| 53 |
+
|
| 54 |
+
# Tabs
|
| 55 |
+
tab1, tab2 = st.tabs(["π§ Single Review", "π Bulk CSV"])
|
| 56 |
+
|
| 57 |
+
def speak(text, lang='en'):
|
| 58 |
+
tts = gTTS(text, lang=lang)
|
| 59 |
+
mp3 = BytesIO()
|
| 60 |
+
tts.write_to_fp(mp3)
|
| 61 |
+
b64 = base64.b64encode(mp3.getvalue()).decode()
|
| 62 |
+
st.markdown(f'<audio controls><source src="data:audio/mp3;base64,{b64}" type="audio/mp3"></audio>', unsafe_allow_html=True)
|
| 63 |
+
mp3.seek(0)
|
| 64 |
+
return mp3
|
| 65 |
+
|
| 66 |
+
# Tab: Single Review
|
| 67 |
+
with tab1:
|
| 68 |
+
st.title("π§ NeuroPulse AI β Multimodal Review Analyzer")
|
| 69 |
+
|
| 70 |
+
review = st.session_state.get("review", "")
|
| 71 |
+
review = st.text_area("π Enter a Review", value=review, height=160)
|
| 72 |
+
|
| 73 |
+
col1, col2, col3 = st.columns(3)
|
| 74 |
+
with col1:
|
| 75 |
+
analyze = st.button("π Analyze")
|
| 76 |
+
with col2:
|
| 77 |
+
if st.button("π² Example"):
|
| 78 |
+
st.session_state["review"] = "App was smooth, but the transaction failed twice on Android."
|
| 79 |
+
st.rerun()
|
| 80 |
+
with col3:
|
| 81 |
+
if st.button("π§Ή Clear"):
|
| 82 |
+
st.session_state["review"] = ""
|
| 83 |
+
st.rerun()
|
| 84 |
+
|
| 85 |
+
if analyze and review:
|
| 86 |
+
with st.spinner("Analyzing..."):
|
| 87 |
+
try:
|
| 88 |
+
payload = {
|
| 89 |
+
"text": review,
|
| 90 |
+
"model": sentiment_model,
|
| 91 |
+
"industry": industry,
|
| 92 |
+
"aspects": use_aspects,
|
| 93 |
+
"follow_up": follow_up,
|
| 94 |
+
"product_category": product_category,
|
| 95 |
+
"device": device_type
|
| 96 |
+
}
|
| 97 |
+
headers = {"X-API-Key": api_token} if api_token else {}
|
| 98 |
+
params = {"smart": "1"} if use_smart_summary else {}
|
| 99 |
+
res = requests.post(f"{backend_url}/analyze/", json=payload, headers=headers, params=params)
|
| 100 |
+
if res.status_code == 200:
|
| 101 |
+
data = res.json()
|
| 102 |
+
st.success("β
Analysis Complete")
|
| 103 |
+
st.subheader("π Summary")
|
| 104 |
+
st.info(data["summary"])
|
| 105 |
+
st.caption(f"π§ Summary Type: {'Smart Summary' if use_smart_summary else 'Standard Model'}")
|
| 106 |
+
st.subheader("π Audio")
|
| 107 |
+
audio = speak(data["summary"], lang=voice_lang)
|
| 108 |
+
st.download_button("β¬οΈ Download Summary Audio", audio.read(), "summary.mp3", mime="audio/mp3")
|
| 109 |
+
st.metric("π Sentiment", data["sentiment"]["label"], delta=f"{data['sentiment']['score']:.2%}")
|
| 110 |
+
st.info(f"π’ Emotion: {data['emotion']}")
|
| 111 |
+
if data.get("aspects"):
|
| 112 |
+
st.subheader("π¬ Aspects")
|
| 113 |
+
for a in data["aspects"]:
|
| 114 |
+
st.write(f"πΉ {a['aspect']}: {a['sentiment']} ({a['score']:.2%})")
|
| 115 |
+
if data.get("follow_up"):
|
| 116 |
+
st.subheader("π€ Follow-Up Response")
|
| 117 |
+
st.warning(data["follow_up"])
|
| 118 |
+
else:
|
| 119 |
+
st.error(f"β API Error: {res.status_code}")
|
| 120 |
+
except Exception as e:
|
| 121 |
+
st.error(f"π« {e}")
|
| 122 |
+
|
| 123 |
+
# Tab: Bulk CSV
|
| 124 |
+
with tab2:
|
| 125 |
+
st.title("π Bulk CSV Upload")
|
| 126 |
+
uploaded_file = st.file_uploader("Upload CSV with `review` column", type="csv")
|
| 127 |
+
if uploaded_file:
|
| 128 |
+
try:
|
| 129 |
+
df = pd.read_csv(uploaded_file)
|
| 130 |
+
if "review" in df.columns:
|
| 131 |
+
st.success(f"β
Loaded {len(df)} reviews")
|
| 132 |
+
|
| 133 |
+
for col in ["industry", "product_category", "device"]:
|
| 134 |
+
if col not in df.columns:
|
| 135 |
+
df[col] = [""] * len(df)
|
| 136 |
+
df[col] = df[col].fillna("").astype(str)
|
| 137 |
+
|
| 138 |
+
if st.button("π Analyze Bulk Reviews"):
|
| 139 |
+
with st.spinner("Processing..."):
|
| 140 |
+
payload = {
|
| 141 |
+
"reviews": df["review"].tolist(),
|
| 142 |
+
"model": sentiment_model,
|
| 143 |
+
"aspects": use_aspects,
|
| 144 |
+
"industry": df["industry"].tolist(),
|
| 145 |
+
"product_category": df["product_category"].tolist(),
|
| 146 |
+
"device": df["device"].tolist()
|
| 147 |
+
}
|
| 148 |
+
headers = {"X-API-Key": api_token} if api_token else {}
|
| 149 |
+
params = {"smart": "1"} if use_smart_summary_bulk else {}
|
| 150 |
+
|
| 151 |
+
res = requests.post(f"{backend_url}/bulk/", json=payload, headers=headers, params=params)
|
| 152 |
+
if res.status_code == 200:
|
| 153 |
+
results = pd.DataFrame(res.json()["results"])
|
| 154 |
+
results["summary_type"] = "Smart" if use_smart_summary_bulk else "Standard"
|
| 155 |
+
st.dataframe(results)
|
| 156 |
+
st.download_button("β¬οΈ Download Results CSV", results.to_csv(index=False), "bulk_results.csv", mime="text/csv")
|
| 157 |
+
else:
|
| 158 |
+
st.error(f"β Bulk Analysis Failed: {res.status_code}")
|
| 159 |
+
else:
|
| 160 |
+
st.error("CSV must contain a column named `review`.")
|
| 161 |
+
except Exception as e:
|
| 162 |
+
st.error(f"β File Error: {e}")
|
logo.png
ADDED
|
Git LFS Details
|
main.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Request, Header, HTTPException
|
| 2 |
+
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
| 3 |
+
from fastapi.openapi.utils import get_openapi
|
| 4 |
+
from fastapi.openapi.docs import get_swagger_ui_html
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
from transformers import pipeline
|
| 7 |
+
from io import StringIO
|
| 8 |
+
import os, csv, logging
|
| 9 |
+
from openai import OpenAI
|
| 10 |
+
from app.model import summarize_review, smart_summarize # import both
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
app = FastAPI(
|
| 14 |
+
title="π§ NeuroPulse AI",
|
| 15 |
+
description="Multilingual GenAI for smarter feedback β summarization, sentiment, emotion, aspects, Q&A and tags.",
|
| 16 |
+
version="2025.1.0",
|
| 17 |
+
openapi_url="/openapi.json",
|
| 18 |
+
docs_url=None,
|
| 19 |
+
redoc_url="/redoc"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
@app.get("/docs", include_in_schema=False)
|
| 23 |
+
def custom_swagger_ui():
|
| 24 |
+
return get_swagger_ui_html(
|
| 25 |
+
openapi_url=app.openapi_url,
|
| 26 |
+
title="π§ Swagger UI - NeuroPulse AI",
|
| 27 |
+
swagger_favicon_url="https://cdn-icons-png.flaticon.com/512/3794/3794616.png",
|
| 28 |
+
swagger_js_url="https://cdn.jsdelivr.net/npm/[email protected]/swagger-ui-bundle.js",
|
| 29 |
+
swagger_css_url="https://cdn.jsdelivr.net/npm/[email protected]/swagger-ui.css",
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
@app.get("/", response_class=HTMLResponse)
|
| 33 |
+
def root():
|
| 34 |
+
return """
|
| 35 |
+
<html>
|
| 36 |
+
<head>
|
| 37 |
+
<title>NeuroPulse AI</title>
|
| 38 |
+
<style>
|
| 39 |
+
body {
|
| 40 |
+
font-family: 'Segoe UI', sans-serif;
|
| 41 |
+
background: linear-gradient(135deg, #f0f4ff, #fef3c7);
|
| 42 |
+
margin: 0;
|
| 43 |
+
padding: 60px;
|
| 44 |
+
text-align: center;
|
| 45 |
+
color: #1f2937;
|
| 46 |
+
}
|
| 47 |
+
.container {
|
| 48 |
+
background: white;
|
| 49 |
+
padding: 40px;
|
| 50 |
+
border-radius: 16px;
|
| 51 |
+
max-width: 800px;
|
| 52 |
+
margin: auto;
|
| 53 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
|
| 54 |
+
animation: fadeIn 1s ease-in-out;
|
| 55 |
+
}
|
| 56 |
+
@keyframes fadeIn {
|
| 57 |
+
from {opacity: 0; transform: translateY(20px);}
|
| 58 |
+
to {opacity: 1; transform: translateY(0);}
|
| 59 |
+
}
|
| 60 |
+
h1 {
|
| 61 |
+
font-size: 36px;
|
| 62 |
+
margin-bottom: 12px;
|
| 63 |
+
color: #4f46e5;
|
| 64 |
+
}
|
| 65 |
+
p {
|
| 66 |
+
font-size: 18px;
|
| 67 |
+
margin-bottom: 32px;
|
| 68 |
+
}
|
| 69 |
+
.btn {
|
| 70 |
+
display: inline-block;
|
| 71 |
+
margin: 8px;
|
| 72 |
+
padding: 14px 24px;
|
| 73 |
+
border-radius: 8px;
|
| 74 |
+
font-weight: 600;
|
| 75 |
+
color: white;
|
| 76 |
+
text-decoration: none;
|
| 77 |
+
background: linear-gradient(90deg, #4f46e5, #6366f1);
|
| 78 |
+
transition: all 0.3s ease;
|
| 79 |
+
}
|
| 80 |
+
.btn:hover {
|
| 81 |
+
transform: translateY(-2px);
|
| 82 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
| 83 |
+
}
|
| 84 |
+
.btn.red {
|
| 85 |
+
background: linear-gradient(90deg, #dc2626, #ef4444);
|
| 86 |
+
}
|
| 87 |
+
</style>
|
| 88 |
+
</head>
|
| 89 |
+
<body>
|
| 90 |
+
<div class="container">
|
| 91 |
+
<h1>π§ Welcome to <strong>NeuroPulse AI</strong></h1>
|
| 92 |
+
<p>Smarter AI feedback analysis β Summarization, Sentiment, Emotion, Aspects, LLM Q&A, and Metadata Tags.</p>
|
| 93 |
+
<a class="btn" href="/docs">π Swagger UI</a>
|
| 94 |
+
<a class="btn red" href="/redoc">π ReDoc</a>
|
| 95 |
+
</div>
|
| 96 |
+
</body>
|
| 97 |
+
</html>
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
# --- Models ---
|
| 101 |
+
class ReviewInput(BaseModel):
|
| 102 |
+
text: str
|
| 103 |
+
model: str = "distilbert-base-uncased-finetuned-sst-2-english"
|
| 104 |
+
industry: str = "Generic"
|
| 105 |
+
aspects: bool = False
|
| 106 |
+
follow_up: str = None
|
| 107 |
+
product_category: str = None
|
| 108 |
+
device: str = None
|
| 109 |
+
|
| 110 |
+
class BulkReviewInput(BaseModel):
|
| 111 |
+
reviews: list[str]
|
| 112 |
+
model: str = "distilbert-base-uncased-finetuned-sst-2-english"
|
| 113 |
+
industry: Optional[list[str]] = None
|
| 114 |
+
aspects: bool = False
|
| 115 |
+
product_category: Optional[list[str]] = None
|
| 116 |
+
device: Optional[list[str]] = None
|
| 117 |
+
|
| 118 |
+
class ChatInput(BaseModel):
|
| 119 |
+
question: str
|
| 120 |
+
context: str
|
| 121 |
+
|
| 122 |
+
class TranslationInput(BaseModel):
|
| 123 |
+
text: str
|
| 124 |
+
target_lang: str = "fr"
|
| 125 |
+
|
| 126 |
+
# --- Auth & Logging ---
|
| 127 |
+
VALID_API_KEY = "my-secret-key"
|
| 128 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
| 129 |
+
|
| 130 |
+
# --- Load Models Once ---
|
| 131 |
+
summarizer = pipeline("summarization", model="sshleifer/distilbart-cnn-12-6")
|
| 132 |
+
emotion_model = pipeline("text-classification", model="j-hartmann/emotion-english-distilroberta-base", top_k=1)
|
| 133 |
+
sentiment_pipelines = {
|
| 134 |
+
"distilbert-base-uncased-finetuned-sst-2-english": pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english"),
|
| 135 |
+
"nlptown/bert-base-multilingual-uncased-sentiment": pipeline("sentiment-analysis", model="nlptown/bert-base-multilingual-uncased-sentiment")
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
# --- Analyze (Bulk) ---
|
| 139 |
+
@app.post("/bulk/")
|
| 140 |
+
async def bulk(data: BulkReviewInput, x_api_key: str = Header(None)):
|
| 141 |
+
if x_api_key != VALID_API_KEY:
|
| 142 |
+
raise HTTPException(status_code=401, detail="Invalid or missing API key")
|
| 143 |
+
|
| 144 |
+
sentiment_pipeline = sentiment_pipelines[data.model]
|
| 145 |
+
summaries = summarizer(data.reviews, max_length=80, min_length=20, truncation=True)
|
| 146 |
+
sentiments = sentiment_pipeline(data.reviews)
|
| 147 |
+
emotions = emotion_model(data.reviews)
|
| 148 |
+
|
| 149 |
+
results = []
|
| 150 |
+
for i, review in enumerate(data.reviews):
|
| 151 |
+
label = sentiments[i]["label"]
|
| 152 |
+
if "star" in label:
|
| 153 |
+
stars = int(label[0])
|
| 154 |
+
label = "NEGATIVE" if stars <= 2 else "NEUTRAL" if stars == 3 else "POSITIVE"
|
| 155 |
+
|
| 156 |
+
result = {
|
| 157 |
+
"review": review,
|
| 158 |
+
"summary": summaries[i]["summary_text"],
|
| 159 |
+
"sentiment": label,
|
| 160 |
+
"emotion": emotions[i][0]["label"],
|
| 161 |
+
"aspects": [],
|
| 162 |
+
"product_category": data.product_category[i] if data.product_category else None,
|
| 163 |
+
"device": data.device[i] if data.device else None,
|
| 164 |
+
"industry": data.industry[i] if data.industry else None,
|
| 165 |
+
}
|
| 166 |
+
results.append(result)
|
| 167 |
+
|
| 168 |
+
return {"results": results}
|
| 169 |
+
|
| 170 |
+
@app.post("/analyze/")
|
| 171 |
+
async def analyze(request: Request, data: ReviewInput, x_api_key: str = Header(None), download: str = None):
|
| 172 |
+
if x_api_key != VALID_API_KEY:
|
| 173 |
+
raise HTTPException(status_code=401, detail="Invalid or missing API key")
|
| 174 |
+
|
| 175 |
+
sentiment_pipeline = sentiment_pipelines.get(data.model)
|
| 176 |
+
summary = smart_summarize(data.text) if request.query_params.get("smart") == "1" else summarize_review(data.text)
|
| 177 |
+
sentiment = sentiment_pipeline(data.text)[0]
|
| 178 |
+
label = sentiment["label"]
|
| 179 |
+
if "star" in label:
|
| 180 |
+
stars = int(label[0])
|
| 181 |
+
label = "NEGATIVE" if stars <= 2 else "NEUTRAL" if stars == 3 else "POSITIVE"
|
| 182 |
+
|
| 183 |
+
emotion = emotion_model(data.text)[0][0]["label"]
|
| 184 |
+
|
| 185 |
+
aspects_list = []
|
| 186 |
+
if data.aspects:
|
| 187 |
+
for asp in ["battery", "price", "camera"]:
|
| 188 |
+
if asp in data.text.lower():
|
| 189 |
+
asp_result = sentiment_pipeline(asp + " " + data.text)[0]
|
| 190 |
+
aspects_list.append({
|
| 191 |
+
"aspect": asp,
|
| 192 |
+
"sentiment": asp_result["label"],
|
| 193 |
+
"score": asp_result["score"]
|
| 194 |
+
})
|
| 195 |
+
|
| 196 |
+
follow_up_response = chat_llm(data.follow_up, data.text) if data.follow_up else None
|
| 197 |
+
|
| 198 |
+
return {
|
| 199 |
+
"summary": summary,
|
| 200 |
+
"sentiment": {"label": label, "score": sentiment["score"]},
|
| 201 |
+
"emotion": emotion,
|
| 202 |
+
"aspects": aspects_list,
|
| 203 |
+
"follow_up": follow_up_response,
|
| 204 |
+
"product_category": data.product_category,
|
| 205 |
+
"device": data.device,
|
| 206 |
+
"industry": data.industry
|
| 207 |
+
}
|
| 208 |
+
# --- Translate ---
|
| 209 |
+
@app.post("/translate/")
|
| 210 |
+
async def translate(data: TranslationInput):
|
| 211 |
+
translator = pipeline("translation", model=f"Helsinki-NLP/opus-mt-en-{data.target_lang}")
|
| 212 |
+
return {"translated_text": translator(data.text)[0]["translation_text"]}
|
| 213 |
+
|
| 214 |
+
# --- LLM Agent Chat ---
|
| 215 |
+
@app.post("/chat/")
|
| 216 |
+
async def chat(input: ChatInput, x_api_key: str = Header(None)):
|
| 217 |
+
if x_api_key != VALID_API_KEY:
|
| 218 |
+
raise HTTPException(status_code=401, detail="Invalid or missing API key")
|
| 219 |
+
return {"response": chat_llm(input.question, input.context)}
|
| 220 |
+
|
| 221 |
+
def chat_llm(question, context):
|
| 222 |
+
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 223 |
+
res = client.chat.completions.create(
|
| 224 |
+
model="gpt-3.5-turbo",
|
| 225 |
+
messages=[
|
| 226 |
+
{"role": "system", "content": "You are a helpful AI review analyst."},
|
| 227 |
+
{"role": "user", "content": f"Context: {context}\nQuestion: {question}"}
|
| 228 |
+
]
|
| 229 |
+
)
|
| 230 |
+
return res.choices[0].message.content.strip()
|
| 231 |
+
|
| 232 |
+
# --- Custom OpenAPI ---
|
| 233 |
+
def custom_openapi():
|
| 234 |
+
if app.openapi_schema:
|
| 235 |
+
return app.openapi_schema
|
| 236 |
+
openapi_schema = get_openapi(
|
| 237 |
+
title=app.title,
|
| 238 |
+
version=app.version,
|
| 239 |
+
description="""
|
| 240 |
+
<b><span style='color:#4f46e5'>NeuroPulse AI</span></b> Β· Smart GenAI Feedback Engine<br>
|
| 241 |
+
Summarize reviews, detect sentiment/emotion, extract aspects, tag metadata, and ask GPT follow-ups.
|
| 242 |
+
""",
|
| 243 |
+
routes=app.routes
|
| 244 |
+
)
|
| 245 |
+
openapi_schema["openapi"] = "3.0.0"
|
| 246 |
+
app.openapi_schema = openapi_schema
|
| 247 |
+
return app.openapi_schema
|
| 248 |
+
|
| 249 |
+
app.openapi = custom_openapi
|
model.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from transformers import pipeline
|
| 4 |
+
import nltk.data
|
| 5 |
+
|
| 6 |
+
# β
Extra: Smart Summarization Imports
|
| 7 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 8 |
+
from sklearn.cluster import KMeans
|
| 9 |
+
from nltk.tokenize import sent_tokenize
|
| 10 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
# π Load HuggingFace Pipelines
|
| 14 |
+
summarizer = pipeline("summarization", model="sshleifer/distilbart-cnn-12-6")
|
| 15 |
+
sentiment_analyzer = pipeline("sentiment-analysis")
|
| 16 |
+
|
| 17 |
+
# π§ Basic Summarization (Abstractive)
|
| 18 |
+
def summarize_review(text):
|
| 19 |
+
return summarizer(text, max_length=60, min_length=10, do_sample=False, no_repeat_ngram_size=3)[0]["summary_text"]
|
| 20 |
+
|
| 21 |
+
# π§ Smart Summarization (Clustered Key Sentences)
|
| 22 |
+
def smart_summarize(text, n_clusters=1):
|
| 23 |
+
"""Improved summarization using clustering on sentence embeddings"""
|
| 24 |
+
tokenizer = nltk.tokenize.PunktSentenceTokenizer() # β
Use default trained Punkt tokenizer
|
| 25 |
+
sentences = tokenizer.tokenize(text)
|
| 26 |
+
|
| 27 |
+
if len(sentences) <= 1:
|
| 28 |
+
return text
|
| 29 |
+
|
| 30 |
+
vectorizer = TfidfVectorizer(stop_words="english")
|
| 31 |
+
tfidf_matrix = vectorizer.fit_transform(sentences)
|
| 32 |
+
|
| 33 |
+
if len(sentences) <= n_clusters:
|
| 34 |
+
return " ".join(sentences)
|
| 35 |
+
|
| 36 |
+
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
|
| 37 |
+
kmeans.fit(tfidf_matrix)
|
| 38 |
+
|
| 39 |
+
avg = []
|
| 40 |
+
for i in range(n_clusters):
|
| 41 |
+
idx = np.where(kmeans.labels_ == i)[0]
|
| 42 |
+
if len(idx) == 0:
|
| 43 |
+
continue
|
| 44 |
+
avg_vector = tfidf_matrix[idx].mean(axis=0).A1.reshape(1, -1) # Convert np.matrix to ndarray
|
| 45 |
+
sim = cosine_similarity(avg_vector, tfidf_matrix[idx])
|
| 46 |
+
most_representative_idx = idx[np.argmax(sim)]
|
| 47 |
+
avg.append(sentences[most_representative_idx])
|
| 48 |
+
|
| 49 |
+
return " ".join(sorted(avg, key=sentences.index))
|
| 50 |
+
|
| 51 |
+
# π Sentiment Detection
|
| 52 |
+
def analyze_sentiment(text):
|
| 53 |
+
result = sentiment_analyzer(text)[0]
|
| 54 |
+
label = result["label"]
|
| 55 |
+
score = result["score"]
|
| 56 |
+
|
| 57 |
+
if "star" in label:
|
| 58 |
+
stars = int(label[0])
|
| 59 |
+
if stars <= 2:
|
| 60 |
+
label = "NEGATIVE"
|
| 61 |
+
elif stars == 3:
|
| 62 |
+
label = "NEUTRAL"
|
| 63 |
+
else:
|
| 64 |
+
label = "POSITIVE"
|
| 65 |
+
|
| 66 |
+
return {
|
| 67 |
+
"label": label,
|
| 68 |
+
"score": score
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
# π₯ Emotion Detection (heuristic-based)
|
| 72 |
+
def detect_emotion(text):
|
| 73 |
+
text_lower = text.lower()
|
| 74 |
+
if "angry" in text_lower or "hate" in text_lower:
|
| 75 |
+
return "anger"
|
| 76 |
+
elif "happy" in text_lower or "love" in text_lower:
|
| 77 |
+
return "joy"
|
| 78 |
+
elif "sad" in text_lower or "disappointed" in text_lower:
|
| 79 |
+
return "sadness"
|
| 80 |
+
elif "confused" in text_lower or "unclear" in text_lower:
|
| 81 |
+
return "confusion"
|
| 82 |
+
else:
|
| 83 |
+
return "neutral"
|
| 84 |
+
|
| 85 |
+
# π§© Aspect-Based Sentiment (mock)
|
| 86 |
+
def extract_aspect_sentiment(text, aspects: list):
|
| 87 |
+
results = {}
|
| 88 |
+
text_lower = text.lower()
|
| 89 |
+
for asp in aspects:
|
| 90 |
+
label = "positive" if asp in text_lower and "not" not in text_lower else "neutral"
|
| 91 |
+
results[asp] = {
|
| 92 |
+
"label": label,
|
| 93 |
+
"confidence": 0.85
|
| 94 |
+
}
|
| 95 |
+
return results
|
| 96 |
+
|
| 97 |
+
# β
Pydantic Schemas for FastAPI
|
| 98 |
+
class ReviewInput(BaseModel):
|
| 99 |
+
text: str
|
| 100 |
+
model: str = "distilbert-base-uncased-finetuned-sst-2-english"
|
| 101 |
+
industry: str = "Generic"
|
| 102 |
+
aspects: bool = False
|
| 103 |
+
follow_up: Optional[str] = None
|
| 104 |
+
product_category: Optional[str] = None
|
| 105 |
+
device: Optional[str] = None
|
| 106 |
+
|
| 107 |
+
class BulkReviewInput(BaseModel):
|
| 108 |
+
reviews: List[str]
|
| 109 |
+
model: str = "distilbert-base-uncased-finetuned-sst-2-english"
|
| 110 |
+
industry: str = "Generic"
|
| 111 |
+
aspects: bool = False
|
| 112 |
+
product_category: Optional[str] = None
|
| 113 |
+
device: Optional[str] = None
|
| 114 |
+
|
| 115 |
+
class TranslationInput(BaseModel):
|
| 116 |
+
text: str
|
| 117 |
+
target_lang: str = "fr"
|
| 118 |
+
|
| 119 |
+
class ChatInput(BaseModel):
|
| 120 |
+
question: str
|
| 121 |
+
context: str
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
transformers
|
| 4 |
+
pandas
|
| 5 |
+
scikit-learn
|
| 6 |
+
nltk
|
| 7 |
+
streamlit
|
| 8 |
+
gtts
|
| 9 |
+
requests
|
| 10 |
+
openai
|