GitHub Actions commited on
Commit
f7bde18
·
0 Parent(s):

chore: code and dataset deploy [skip ci]

Browse files
Files changed (6) hide show
  1. README.md +58 -0
  2. app.py +242 -0
  3. data/digikala_samples.csv +0 -0
  4. prepare_data.py +121 -0
  5. processor.py +157 -0
  6. requirements.txt +10 -0
README.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: NLP Sentiment Analysis Digikala
3
+ emoji: 📊
4
+ colorFrom: red
5
+ colorTo: pink
6
+ sdk: streamlit
7
+ sdk_version: 1.58.0
8
+ python_version: 3.12
9
+ app_file: app.py
10
+ pinned: false
11
+ ---
12
+
13
+ # سیستم هوشمند تحلیل احساسات نظرات دیجی‌کالا (RAG-based)
14
+
15
+ این پروژه یک سیستم پیشرفته برای تحلیل نظرات کاربران دیجی‌کالا است که با استفاده از معماری **RAG (Retrieval-Augmented Generation)** طراحی شده است. این سیستم نه تنها احساس نظر (مثبت/منفی) را تشخیص می‌دهد، بلکه با جستجو در میان ۲۰۰۰ نظر واقعی دیگر، دلیلی برای تحلیل خود ارائه می‌کند.
16
+
17
+ ---
18
+
19
+ ## 🚀 ویژگی‌های کلیدی
20
+ - **تحلیل احساسات ۵ سطحی**: استفاده از مدل bert-base-multilingual-uncased-sentiment برای تشخیص دقیق میزان رضایت.
21
+ - **تولید توضیح هوشمند**: استفاده از GPT-2 فارسی برای ارائه دلیل بر اساس شواهد.
22
+ - **جستجوی معنایی سریع**: بهره‌گیری از FAISS برای یافتن نظرات مشابه در کمتر از چند میلی‌ثانیه.
23
+ - **دیتابیس غنی**: ترکیب ۴ دیتاست معتبر از نظرات دیجی‌کالا (شامل نظرات ویدئویی و متنی).
24
+ - **رابط کاربری زیبا**: طراحی شده با Streamlit و پشتیبانی کامل از زبان فارسی (RTL).
25
+
26
+ ---
27
+
28
+ ## 📚 مستندات و آموزش (Wiki)
29
+ ما یک مستندات کامل و خط‌به‌خط برای این پروژه تهیه کرده‌ایم که در پوشه `wiki/` قرار دارد. اگر می‌خواهید بدانید کدها چگونه کار می‌کنند یا قصد ارائه پروژه را دارید، حتماً این بخش را ببینید:
30
+ 👉 **[مشاهده دانشنامه پروژه (Wiki)](wiki/README.md)**
31
+
32
+ ---
33
+
34
+ ## 🛠 نحوه اجرا در محیط محلی
35
+ ۱. ابتدا کتابخانه‌های مورد نیاز را نصب کنید:
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ ```
39
+
40
+ ۲. داده‌ها و ایندکس FAISS را آماده کنید:
41
+ ```bash
42
+ python prepare_data.py
43
+ ```
44
+
45
+ ۳. برنامه را اجرا کنید:
46
+ ```bash
47
+ streamlit run app.py
48
+ ```
49
+
50
+ ---
51
+
52
+ ## ☁️ میزبانی و استقرار
53
+ این پروژه به صورت خودکار در **Hugging Face Spaces** میزبانی می‌شود. برای حفظ سرعت و بهینگی، فایل‌های مستندات و تست‌ها در نسخه سرور آپلود نمی‌شوند.
54
+ 👉 [لینک مشاهده زنده پروژه](https://huggingface.co/spaces/tahatehrani/nlp-segment-analysis)
55
+
56
+ ---
57
+ **توسعه‌دهنده:** Taha Tehrani Nasab
58
+ **(پروژه درس پردازش زبان طبیعی - NLP)**
app.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import streamlit as st
3
+ import pandas as pd
4
+ import plotly.express as px
5
+ from processor import SentimentRAG
6
+ import time
7
+
8
+ # Page Config
9
+ st.set_page_config(
10
+ page_title="تحلیل هوشمند نظرات دیجی‌کالا",
11
+ page_icon="📊",
12
+ layout="wide",
13
+ initial_sidebar_state="expanded"
14
+ )
15
+
16
+ # Custom CSS for Professional UI
17
+ st.markdown("""
18
+ <style>
19
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;700&display=swap');
20
+
21
+ :root {
22
+ --primary-color: #ef4056;
23
+ --bg-light: #f8f9fa;
24
+ --text-dark: #343a40;
25
+ }
26
+
27
+ html, body, [class*="css"] {
28
+ font-family: 'Vazirmatn', sans-serif;
29
+ direction: rtl;
30
+ text-align: right;
31
+ }
32
+
33
+ .main {
34
+ background-color: var(--bg-light);
35
+ }
36
+
37
+ .stMetric {
38
+ background-color: white;
39
+ padding: 15px;
40
+ border-radius: 12px;
41
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
42
+ border: 1px solid #eee;
43
+ }
44
+
45
+ .stButton>button {
46
+ width: 100%;
47
+ border-radius: 8px;
48
+ height: 3.5em;
49
+ background-color: var(--primary-color);
50
+ color: white;
51
+ font-weight: bold;
52
+ transition: all 0.3s ease;
53
+ border: none;
54
+ }
55
+
56
+ .stButton>button:hover {
57
+ background-color: #d8364b;
58
+ transform: translateY(-2px);
59
+ box-shadow: 0 4px 12px rgba(239, 64, 86, 0.3);
60
+ }
61
+
62
+ .sentiment-box {
63
+ padding: 25px;
64
+ border-radius: 15px;
65
+ margin: 20px 0;
66
+ border: none;
67
+ box-shadow: 0 10px 20px rgba(0,0,0,0.05);
68
+ transition: transform 0.3s ease;
69
+ }
70
+
71
+ .sentiment-box:hover {
72
+ transform: scale(1.01);
73
+ }
74
+
75
+ .positive { background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); border-right: 8px solid #4caf50; color: #2e7d32; }
76
+ .negative { background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%); border-right: 8px solid #f44336; color: #c62828; }
77
+ .neutral { background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); border-right: 8px solid #ff9800; color: #ef6c00; }
78
+
79
+ .custom-header {
80
+ background-color: white;
81
+ padding: 20px;
82
+ border-radius: 0 0 20px 20px;
83
+ box-shadow: 0 4px 10px rgba(0,0,0,0.05);
84
+ margin-bottom: 30px;
85
+ }
86
+
87
+ .stExpander {
88
+ border: none !important;
89
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
90
+ border-radius: 10px !important;
91
+ background-color: white;
92
+ }
93
+
94
+ div[data-testid="stDataFrame"] {
95
+ direction: rtl;
96
+ text-align: right;
97
+ border-radius: 10px;
98
+ overflow: hidden;
99
+ }
100
+ </style>
101
+ """, unsafe_allow_html=True)
102
+
103
+ @st.cache_resource
104
+ def load_rag():
105
+ # Data is pre-prepared by GitHub Actions or handled via online fallback in SentimentRAG
106
+ return SentimentRAG()
107
+
108
+ def get_rag_instance_safe():
109
+ """Returns the RAG instance ONLY if it has already been initialized."""
110
+ if SentimentRAG._instance and hasattr(SentimentRAG._instance, 'initialized') and SentimentRAG._instance.initialized:
111
+ return SentimentRAG._instance
112
+ return None
113
+
114
+ # Sidebar
115
+ with st.sidebar:
116
+ st.image("https://upload.wikimedia.org/wikipedia/commons/2/29/Digikala_logo.svg", width=200)
117
+ st.title("کنترل پنل")
118
+ st.markdown("---")
119
+ st.info("این سیستم با ترکیب مدل‌های BERT و معماری RAG، تحلیل دقیقی از نظرات ارائه می‌دهد.")
120
+
121
+ with st.expander("تنظیمات پیشرفته"):
122
+ k_val = st.slider("تعداد نظرات مشابه (RAG)", 1, 5, 3)
123
+ clear_cache = st.button("بازنشانی حافظه")
124
+ if clear_cache:
125
+ st.cache_resource.clear()
126
+ st.rerun()
127
+
128
+ # Header Section
129
+ st.markdown("""
130
+ <div class="custom-header">
131
+ <h1 style='color: #ef4056; margin: 0;'>سامانه تحلیل هوشمند نظرات دیجی‌کالا</h1>
132
+ <p style='color: #666; margin-top: 5px;'>استخراج احساسات و تولید استدلال مبتنی بر داده (معماری RAG)</p>
133
+ </div>
134
+ """, unsafe_allow_html=True)
135
+
136
+ tab1, tab2 = st.tabs(["تحلیل متن", "داشبورد آماری"])
137
+
138
+ with tab1:
139
+ col1, col2 = st.columns([3, 2], gap="large")
140
+
141
+ with col1:
142
+ st.markdown("### ورود اطلاعات")
143
+ user_input = st.text_area(
144
+ "نظر خود را برای تحلیل وارد کنید:",
145
+ placeholder="مثال: این محصول عالی است اما بسته‌بندی آن ضعیف بود...",
146
+ height=200,
147
+ help="متن نظر را اینجا تایپ کنید."
148
+ )
149
+ analyze_btn = st.button("شروع تحلیل")
150
+
151
+ with col2:
152
+ if analyze_btn and user_input:
153
+ with st.spinner("در حال پردازش و تحلیل هوشمند..."):
154
+ start_time = time.time()
155
+ # Check if data exists locally to inform user
156
+ if not os.path.exists("data/digikala_samples.csv"):
157
+ st.warning("⚠️ دیتابیس محلی یافت نشد. در حال فراخوانی آنلاین داده‌ها...")
158
+
159
+ rag = load_rag()
160
+ score, confidence = rag.get_sentiment(user_input)
161
+ explanation = rag.generate_explanation(user_input, score)
162
+ similar = rag.retrieve_similar(user_input, k=k_val)
163
+ elapsed = time.time() - start_time
164
+
165
+ if score > 3:
166
+ css_class, label = "positive", "مثبت"
167
+ elif score < 3:
168
+ css_class, label = "negative", "منفی"
169
+ else:
170
+ css_class, label = "neutral", "خنثی"
171
+
172
+ st.markdown(f"""
173
+ <div class="sentiment-box {css_class}">
174
+ <h2 style='margin:0;'>وضعیت: {label}</h2>
175
+ <hr style='border: 0.5px solid rgba(0,0,0,0.1);'>
176
+ <p style='font-size: 1.1em;'><b>امتیاز شدت:</b> {score} از ۵</p>
177
+ <p style='font-size: 1.1em;'><b>ضریب اطمینان:</b> {confidence:.2%}</p>
178
+ <small>زمان پردازش: {elapsed:.2f} ثانیه</small>
179
+ </div>
180
+ """, unsafe_allow_html=True)
181
+
182
+ st.markdown("### دلیل و استدلال:")
183
+ st.info(explanation)
184
+ else:
185
+ st.info("متن نظر را وارد کرده و دکمه تحلیل را بزنید تا نتایج اینجا نمایش داده شوند.")
186
+
187
+ if analyze_btn and user_input:
188
+ st.markdown("---")
189
+ with st.expander("شواهد بازیابی شده (RAG Context)", expanded=False):
190
+ for i, s in enumerate(similar, 1):
191
+ st.write(f"**{i}.** {s}")
192
+
193
+ with tab2:
194
+ st.header("داده‌های مرجع")
195
+ try:
196
+ data_file = "data/digikala_samples.csv"
197
+ df = None
198
+ if os.path.exists(data_file):
199
+ df = pd.read_csv(data_file)
200
+ else:
201
+ # ONLY attempt to retrieve from RAG if it is already initialized to avoid OOM/Bottlenecks
202
+ rag_instance = get_rag_instance_safe()
203
+ if rag_instance:
204
+ df = rag_instance.df
205
+
206
+ if df is not None:
207
+ m1, m2, m3, m4 = st.columns(4)
208
+ m1.metric("تعداد نظرات", len(df))
209
+ m2.metric("مدل", "bert-base")
210
+ m3.metric("معماری", "RAG")
211
+ m4.metric("زبان", "فارسی")
212
+
213
+ c1, c2 = st.columns(2)
214
+
215
+ with c1:
216
+ df['length'] = df['text'].str.len()
217
+ fig_hist = px.histogram(
218
+ df, x="length",
219
+ title="توزیع طول نظرات",
220
+ color_discrete_sequence=['#ef4056'],
221
+ labels={'length': 'طول متن'}
222
+ )
223
+ fig_hist.update_layout(plot_bgcolor='white')
224
+ st.plotly_chart(fig_hist, width='stretch')
225
+
226
+ with c2:
227
+ st.markdown("### پیش‌نمایش داده‌ها")
228
+ st.dataframe(df.head(15), width='stretch')
229
+ else:
230
+ st.warning("دیتابیس نظرات یافت نشد. لطفاً منتظر بروزرسانی خودکار بمانید یا دکمه تحلیل (در زبانه اول) را بزنید تا داده‌ها بارگذاری شوند.")
231
+
232
+ except Exception as e:
233
+ st.error(f"⚠️ خطا در بارگذاری دیتابیس: {e}")
234
+
235
+ # Footer
236
+ st.markdown("---")
237
+ st.markdown("""
238
+ <div style='text-align: center; color: #888; padding: 20px;'>
239
+ (پروژه NLP - Taha Tehrani Nasab)<br>
240
+ <small>قدرت گرفته از Hugging Face Transformers & FAISS</small>
241
+ </div>
242
+ """, unsafe_allow_html=True)
data/digikala_samples.csv ADDED
The diff for this file is too large to render. See raw diff
 
prepare_data.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from datasets import load_dataset
3
+ import re
4
+ import os
5
+ import sys
6
+ import numpy as np
7
+ from sentence_transformers import SentenceTransformer
8
+ import faiss
9
+
10
+ def clean_text(text):
11
+ if not isinstance(text, str):
12
+ return ""
13
+ # Remove extra spaces and newlines
14
+ text = re.sub(r'\s+', ' ', text).strip()
15
+ return text
16
+
17
+ def get_samples_from_stream(ds_name, split, token, target_count, buffer_size=2000):
18
+ print(f"Streaming samples from {ds_name}...")
19
+ try:
20
+ # EhsanShahbazi datasets might have different structures, we try to handle them
21
+ ds = load_dataset(ds_name, split=split, token=token if token else None, streaming=True)
22
+ samples = []
23
+ count = 0
24
+ for item in ds:
25
+ # Common column names for text in these datasets
26
+ potential_cols = ['text', 'Text', 'comment', 'Comment', 'content', 'Content', 'body']
27
+ text_val = None
28
+ for col in potential_cols:
29
+ if col in item:
30
+ text_val = item[col]
31
+ break
32
+
33
+ if text_val:
34
+ cleaned = clean_text(text_val)
35
+ # Filter for meaningful length
36
+ if len(cleaned) > 25:
37
+ samples.append({'text': cleaned})
38
+ count += 1
39
+
40
+ # Stop if we have enough samples or reached buffer limit
41
+ if count >= buffer_size or count >= target_count * 3:
42
+ break
43
+
44
+ if not samples:
45
+ return None
46
+
47
+ df = pd.DataFrame(samples)
48
+ return df
49
+ except Exception as e:
50
+ print(f"Error loading {ds_name}: {e}")
51
+ return None
52
+
53
+ def fetch_all_data(target_total=2000):
54
+ """Fetches data from multiple Digikala comment datasets on Hugging Face."""
55
+ hf_token = os.getenv("HUGGINGFACE_TOKEN")
56
+ if hf_token == "": hf_token = None
57
+ datasets_to_load = [
58
+ ("fibonacciai/Digikala-Comments", "train"),
59
+ ("ParsiAI/digikala-sentiment-analysis", "train"),
60
+ ("EhsanShahbazi/digikala-comments", "train"),
61
+ ("EhsanShahbazi/digikala-comments-with-media", "train")
62
+ ]
63
+
64
+ dfs = []
65
+ per_source_target = target_total // len(datasets_to_load)
66
+
67
+ for ds_name, split in datasets_to_load:
68
+ df = get_samples_from_stream(ds_name, split, hf_token, per_source_target)
69
+ if df is not None:
70
+ dfs.append(df)
71
+
72
+ if not dfs:
73
+ print("Warning: No datasets could be loaded. Using mock data for safety.")
74
+ return pd.DataFrame({'text': ["این یک نظر تستی است برای زمانی که دیتاست در دسترس نیست."] * 10})
75
+
76
+ processed_dfs = []
77
+ # Aim for balanced sampling
78
+ per_source = target_total // len(dfs)
79
+
80
+ for source_df in dfs:
81
+ source_df = source_df.drop_duplicates(subset=['text'])
82
+ if not source_df.empty:
83
+ s_size = min(len(source_df), per_source)
84
+ processed_dfs.append(source_df.sample(n=s_size, random_state=42))
85
+
86
+ if not processed_dfs:
87
+ return None
88
+
89
+ df_sample = pd.concat(processed_dfs, ignore_index=True)
90
+ # Shuffle the final dataset
91
+ df_sample = df_sample.sample(frac=1, random_state=42).reset_index(drop=True)
92
+ print(f"Total samples collected: {len(df_sample)}")
93
+ return df_sample
94
+
95
+ def generate_faiss_index(df, output_dir="data"):
96
+ print("Generating FAISS index...")
97
+ model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
98
+ texts = df['text'].tolist()
99
+ embeddings = model.encode(texts, show_progress_bar=True)
100
+ index = faiss.IndexFlatL2(embeddings.shape[1])
101
+ index.add(np.array(embeddings).astype('float32'))
102
+ os.makedirs(output_dir, exist_ok=True)
103
+ faiss.write_index(index, os.path.join(output_dir, "faiss_index.bin"))
104
+ print(f"FAISS index saved to {output_dir}/faiss_index.bin")
105
+
106
+ def prepare_data():
107
+ df = fetch_all_data()
108
+ if df is not None:
109
+ os.makedirs("data", exist_ok=True)
110
+ df.to_csv("data/digikala_samples.csv", index=False)
111
+ print(f"Data saved to data/digikala_samples.csv")
112
+ generate_faiss_index(df)
113
+
114
+ if __name__ == "__main__":
115
+ try:
116
+ prepare_data()
117
+ print("Data preparation completed successfully.")
118
+ os._exit(0)
119
+ except Exception as e:
120
+ print(f"Fatal error during data preparation: {e}")
121
+ os._exit(1)
processor.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import torch
4
+ from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
5
+ from sentence_transformers import SentenceTransformer
6
+ import faiss
7
+ import os
8
+ import sys
9
+
10
+ class SentimentRAG:
11
+ _instance = None
12
+
13
+ def __new__(cls, *args, **kwargs):
14
+ if not cls._instance:
15
+ cls._instance = super(SentimentRAG, cls).__new__(cls)
16
+ return cls._instance
17
+
18
+ def __init__(self, data_path="data/digikala_samples.csv", index_path=None):
19
+ if hasattr(self, 'initialized') and self.initialized:
20
+ return
21
+
22
+ if index_path is None:
23
+ if data_path == "data/digikala_samples.csv":
24
+ index_path = "data/faiss_index.bin"
25
+ else:
26
+ index_path = data_path.replace(".csv", ".bin")
27
+
28
+ print(f"Initializing SentimentRAG models (Online Optimized)...")
29
+ torch.set_num_threads(2)
30
+
31
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
32
+ hf_token = os.getenv("HUGGINGFACE_TOKEN")
33
+ if hf_token == "": hf_token = None
34
+
35
+ # 1. Sentiment Model
36
+ self.sentiment_pipe = pipeline(
37
+ "sentiment-analysis",
38
+ model="nlptown/bert-base-multilingual-uncased-sentiment",
39
+ device=-1 if self.device == "cpu" else 0,
40
+ token=hf_token if hf_token else None,
41
+ model_kwargs={"low_cpu_mem_usage": True} if self.device == "cpu" else {}
42
+ )
43
+
44
+ # 2. Embedding Model
45
+ self.embed_model = SentenceTransformer(
46
+ 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2',
47
+ use_auth_token=hf_token if hf_token else None
48
+ )
49
+
50
+ # 3. Reasoning Model
51
+ self.gen_tokenizer = AutoTokenizer.from_pretrained("HooshvareLab/gpt2-fa-comment", token=hf_token if hf_token else None)
52
+ self.gen_model = AutoModelForCausalLM.from_pretrained(
53
+ "HooshvareLab/gpt2-fa-comment",
54
+ token=hf_token if hf_token else None,
55
+ low_cpu_mem_usage=True
56
+ ).to(self.device)
57
+
58
+ # Load Data & Index
59
+ self._load_resources(data_path, index_path)
60
+
61
+ self.initialized = True
62
+
63
+ def _load_resources(self, data_path, index_path):
64
+ self.df = None
65
+ self.texts = []
66
+
67
+ # Priority 1: Local file (if exists and is not LFS pointer)
68
+ if os.path.exists(data_path):
69
+ try:
70
+ temp_df = pd.read_csv(data_path)
71
+ if len(temp_df) > 0 and 'version https://git-lfs' in str(temp_df.columns[0]):
72
+ print("Found LFS pointer. Skipping local load.")
73
+ else:
74
+ self.df = temp_df
75
+ self.texts = self.df['text'].tolist()
76
+ print(f"Loaded {len(self.texts)} samples from local file.")
77
+ except Exception as e:
78
+ print(f"Local CSV load failed: {e}")
79
+
80
+ # Priority 2: Online streaming fallback
81
+ if self.df is None:
82
+ print("Fetching data from Hugging Face Hub (Streaming)...")
83
+ try:
84
+ from prepare_data import fetch_all_data
85
+ self.df = fetch_all_data()
86
+ if self.df is not None:
87
+ self.texts = self.df['text'].tolist()
88
+ print(f"Streamed {len(self.texts)} samples online.")
89
+ except Exception as e:
90
+ print(f"Online data streaming failed: {e}")
91
+
92
+ if self.df is None:
93
+ raise FileNotFoundError("System failed to load any data (Local/Online).")
94
+
95
+ # FAISS Index Handling
96
+ if os.path.exists(index_path):
97
+ try:
98
+ loaded_index = faiss.read_index(index_path)
99
+ if loaded_index.ntotal == len(self.texts):
100
+ self.index = loaded_index
101
+ print("Loaded pre-generated FAISS index.")
102
+ return
103
+ except Exception:
104
+ pass
105
+
106
+ print("Building FAISS index in memory...")
107
+ self._build_index()
108
+
109
+ def _build_index(self):
110
+ embeddings = self.embed_model.encode(self.texts, show_progress_bar=False)
111
+ self.index = faiss.IndexFlatL2(embeddings.shape[1])
112
+ self.index.add(np.array(embeddings).astype('float32'))
113
+
114
+ def get_sentiment(self, text):
115
+ result = self.sentiment_pipe(text[:512])[0]
116
+ score = int(result['label'].split()[0])
117
+ return score, result['score']
118
+
119
+ def retrieve_similar(self, text, k=2):
120
+ k = min(k, len(self.texts))
121
+ if k <= 0: return []
122
+ query_vec = self.embed_model.encode([text])
123
+ distances, indices = self.index.search(np.array(query_vec).astype('float32'), k)
124
+ return [self.texts[i] for i in indices[0]]
125
+
126
+ def generate_explanation(self, text, sentiment_score):
127
+ similar_comments = self.retrieve_similar(text, k=2)
128
+ context = " ".join([f"نمونه: {c[:60]}" for c in similar_comments])
129
+ sentiment_label = "مثبت" if sentiment_score > 3 else "منفی" if sentiment_score < 3 else "خنثی"
130
+
131
+ prompt = f"متن: {text[:100]}\nاحساس: {sentiment_label}\nشواهد: {context}\nدلیل فنی:"
132
+ inputs = self.gen_tokenizer(prompt, return_tensors="pt", truncation=True, max_length=400).to(self.device)
133
+
134
+ with torch.no_grad():
135
+ outputs = self.gen_model.generate(
136
+ **inputs,
137
+ max_new_tokens=50,
138
+ do_sample=True,
139
+ top_p=0.9,
140
+ temperature=0.7,
141
+ pad_token_id=self.gen_tokenizer.eos_token_id
142
+ )
143
+
144
+ full_text = self.gen_tokenizer.decode(outputs[0], skip_special_tokens=True)
145
+ if "دلیل فنی:" in full_text:
146
+ explanation = full_text.split("دلیل فنی:")[-1].strip()
147
+ else:
148
+ explanation = "تحلیل بر اساس الگوهای متنی مشابه در پایگاه داده دیجی‌کالا انجام شده است."
149
+
150
+ return explanation if len(explanation) > 10 else "این نظر به دلیل شباهت با نظرات ثبت شده قبلی و الگوهای کلامی شناسایی شده، دارای بار احساسی مشخص شده است."
151
+
152
+ if __name__ == "__main__":
153
+ rag = SentimentRAG()
154
+ test_text = "کیفیتش خوبه ولی قیمتش بالاست"
155
+ score, conf = rag.get_sentiment(test_text)
156
+ print(f"Sentiment: {score}, Confidence: {conf}")
157
+ print(f"Reason: {rag.generate_explanation(test_text, score)}")
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit==1.58.0
2
+ transformers
3
+ torch --index-url https://download.pytorch.org/whl/cpu
4
+ sentence-transformers
5
+ faiss-cpu
6
+ pandas
7
+ datasets
8
+ plotly
9
+ scikit-learn
10
+ huggingface_hub