sumittech commited on
Commit
79e7c20
·
verified ·
1 Parent(s): 40f6468

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -241
app.py CHANGED
@@ -9,7 +9,7 @@ from fastapi.responses import HTMLResponse, JSONResponse
9
  from pydantic import BaseModel
10
  from rank_bm25 import BM25Okapi
11
  from sentence_transformers import SentenceTransformer
12
- from transformers import T5Tokenizer, T5ForConditionalGeneration
13
  from collections import Counter
14
  from typing import Optional
15
 
@@ -28,9 +28,6 @@ STOPWORDS = {
28
  'would','could','should','its','if','about','than','then','too','s','t'
29
  }
30
 
31
- def _check(name):
32
- import __main__
33
- return hasattr(__main__, name)
34
 
35
  def preprocess(text, remove_stops=False):
36
  if not isinstance(text, str) or not text.strip(): return ''
@@ -44,9 +41,7 @@ def preprocess(text, remove_stops=False):
44
  return text
45
 
46
 
47
- # ── Load everything at startup ─────────────────────────────────
48
  CSV_PATH = os.getenv('CSV_PATH', 'Dataset-SA.csv')
49
-
50
  print('Loading data...')
51
  df_raw = pd.read_csv(CSV_PATH)
52
  mask_bad = pd.to_numeric(df_raw[COL_RATE], errors='coerce').isna()
@@ -67,37 +62,32 @@ df = df[df['clean_text'].str.len() > 15].reset_index(drop=True)
67
  df_sample = df.sample(n=min(1000, len(df)), random_state=42).reset_index(drop=True)
68
  print(f'Data: {len(df):,} rows | sample: {len(df_sample):,}')
69
 
70
- print('Loading SBERT (multilingual)...')
71
  sbert = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
72
  EMBED_DIM = sbert.get_sentence_embedding_dimension()
73
- embeddings = sbert.encode(
74
- df_sample['clean_text'].tolist(), batch_size=16,
75
- show_progress_bar=True, convert_to_numpy=True, normalize_embeddings=False
76
- )
77
-
78
- print('Building FAISS index...')
79
  emb_norm = embeddings.copy().astype('float32')
80
  faiss.normalize_L2(emb_norm)
81
  index_flat = faiss.IndexFlatIP(EMBED_DIM)
82
  index_flat.add(emb_norm)
83
-
84
  print('Building BM25...')
85
  tokenized_corpus = [doc.split() for doc in df_sample['clean_text_bm25'].tolist()]
86
  bm25 = BM25Okapi(tokenized_corpus)
87
-
88
  print('Loading Flan-T5...')
89
- _tok = T5Tokenizer.from_pretrained('google/flan-t5-base')
90
- _model = T5ForConditionalGeneration.from_pretrained('google/flan-t5-base')
91
  _model.eval()
92
-
93
- print('Building autocomplete index...')
94
  _all_words = Counter()
95
- for txt in df_sample['clean_text_bm25'].sample(min(2000, len(df_sample))):
96
  _all_words.update(txt.split())
97
  _top_words = [w for w, c in _all_words.most_common(500) if len(w) > 3]
98
  _product_names = df_sample[COL_PRODUCT].str.lower().str[:50].drop_duplicates().head(200).tolist()
99
  _autocomplete_corpus = list(set(_top_words + _product_names))
100
- print(f'All models ready! Autocomplete: {len(_autocomplete_corpus)} suggestions')
 
101
 
102
  # ── Search cache (for performance) ────────────────────────────
103
  _cache = {}
@@ -183,16 +173,6 @@ def _similar(doc_id, k=5):
183
  if len(rows) == k: break
184
  return rows
185
 
186
- # ── Build autocomplete index ───────────────────────────────────
187
- print('Building autocomplete index...')
188
- _all_words = Counter()
189
- for txt in df_sample['clean_text_bm25'].sample(min(3000, len(df_sample))):
190
- _all_words.update(txt.split())
191
- _top_words = [w for w, c in _all_words.most_common(500) if len(w) > 3]
192
- _product_names = df_sample[COL_PRODUCT].str.lower().str[:50].drop_duplicates().head(200).tolist()
193
- _autocomplete_corpus = list(set(_top_words + _product_names))
194
- print(f'Autocomplete: {len(_autocomplete_corpus)} suggestions ready')
195
-
196
  # ── FastAPI ────────────────────────────────────────────────────
197
  app_ui = FastAPI(title='FlipSearch Ultimate')
198
  app_ui.add_middleware(CORSMiddleware, allow_origins=['*'], allow_methods=['*'], allow_headers=['*'])
@@ -281,237 +261,105 @@ async def autocomplete(q: str = ''):
281
  matches = [w for w in _autocomplete_corpus if w.startswith(ql)][:8]
282
  return {'suggestions': matches}
283
 
284
- @app_ui.post('/analyse')
285
- def _analyse_uploaded_df(udf):
286
- report = {}
287
- report['rows'] = int(len(udf))
288
- report['columns'] = list(udf.columns)
289
- report['missing'] = {c: int(udf[c].isna().sum()) for c in udf.columns}
290
-
291
- text_col = next((c for c in ['review','Review','text','Text','comment',
292
- 'Comment','description','feedback','body']
293
- if c in udf.columns), None)
294
- if text_col is None:
295
- obj = udf.select_dtypes(include='object').columns
296
- text_col = obj[0] if len(obj) > 0 else None
297
-
298
- rating_col = next((c for c in ['rating','Rating','rate','Rate',
299
- 'stars','score','Score']
300
- if c in udf.columns), None)
301
-
302
- sent_col = next((c for c in ['sentiment','Sentiment','label','Label']
303
- if c in udf.columns), None)
304
-
305
- report['detected'] = {
306
- 'text_column' : text_col,
307
- 'rating_column' : rating_col,
308
- 'sentiment_column': sent_col
309
- }
310
-
311
- if text_col:
312
- lengths = udf[text_col].dropna().astype(str).apply(len)
313
- words = udf[text_col].dropna().astype(str).apply(lambda x: len(x.split()))
314
- report['text_stats'] = {
315
- 'avg_length' : round(float(lengths.mean()), 1),
316
- 'max_length' : int(lengths.max()),
317
- 'min_length' : int(lengths.min()),
318
- 'avg_words' : round(float(words.mean()), 1),
319
- 'total_non_empty': int(
320
- (udf[text_col].notna() &
321
- (udf[text_col].astype(str).str.len() > 0)).sum()
322
- )
323
- }
324
- all_words = ' '.join(
325
- udf[text_col].dropna().astype(str).str.lower().tolist()
326
- )
327
- word_freq = Counter(
328
- w for w in re.sub(r'[^a-z\s]', '', all_words).split()
329
- if w not in STOPWORDS and len(w) > 2
330
- )
331
- report['top_words'] = [
332
- {'word': w, 'count': c}
333
- for w, c in word_freq.most_common(15)
334
- ]
335
- else:
336
- report['text_stats'] = {}
337
- report['top_words'] = []
338
-
339
- if rating_col:
340
- rvals = pd.to_numeric(udf[rating_col], errors='coerce').dropna()
341
- report['rating_stats'] = {
342
- 'avg' : round(float(rvals.mean()), 2),
343
- 'distribution': {
344
- str(int(k)): int(v)
345
- for k, v in rvals.value_counts().sort_index().items()
346
- }
347
- }
348
- else:
349
- report['rating_stats'] = {}
350
-
351
- if sent_col:
352
- svals = udf[sent_col].dropna().astype(str).str.lower().str.strip()
353
- report['sentiment_stats'] = {
354
- str(k): int(v) for k, v in svals.value_counts().items()
355
- }
356
- else:
357
- report['sentiment_stats'] = {}
358
-
359
- if text_col:
360
- sample_reviews = udf[text_col].dropna().astype(str).head(8).tolist()
361
- sample_text = ' '.join(sample_reviews)[:800]
362
- prompt = (
363
- 'You are a data analyst reviewing customer feedback.\n'
364
- f'Reviews: {sample_text}\n\n'
365
- 'Summarise: (1) overall satisfaction '
366
- '(2) common praise (3) common complaints (4) product quality.\n'
367
- 'Answer:'
368
- )
369
- inputs = _tok(
370
- prompt, return_tensors='pt',
371
- max_length=512, truncation=True
372
- )
373
- with torch.no_grad():
374
- out = _model.generate(
375
- input_ids=inputs['input_ids'],
376
- attention_mask=inputs['attention_mask'],
377
- max_new_tokens=120, num_beams=2,
378
- early_stopping=True, no_repeat_ngram_size=3
379
- )
380
- report['ai_summary'] = _tok.decode(
381
- out[0], skip_special_tokens=True
382
- ).strip()
383
- else:
384
- report['ai_summary'] = 'No text column detected for analysis.'
385
-
386
- return report
387
-
388
- @app_ui.post('/search')
389
- async def search(req: SearchRequest):
390
- t0 = time.time()
391
- try:
392
- answer = None
393
- if req.mode == 'semantic':
394
- results = _semantic(req.query, req.k)
395
- elif req.mode == 'rag':
396
- results, answer = _rag(req.query, req.k, req.alpha)
397
- else:
398
- results = _hybrid(req.query, req.k, req.alpha)
399
- sent_dist = {}
400
- rating_dist = {}
401
- for r in results:
402
- sent_dist[r['sentiment']] = sent_dist.get(r['sentiment'], 0) + 1
403
- rating_dist[str(r['rating'])] = rating_dist.get(str(r['rating']), 0) + 1
404
- return {
405
- 'results' : results,
406
- 'answer' : answer,
407
- 'latency' : round(time.time()-t0, 2),
408
- 'mode' : req.mode,
409
- 'sent_dist' : sent_dist,
410
- 'rating_dist': rating_dist,
411
- }
412
- except Exception as e:
413
- return {'results':[], 'answer':None, 'latency':0, 'mode':req.mode, 'error':str(e)}
414
-
415
- @app_ui.get('/similar/{doc_id}')
416
- async def similar(doc_id: int, k: int = 5):
417
- try:
418
- return {'results': _similar(doc_id, k)}
419
- except Exception as e:
420
- return {'results': [], 'error': str(e)}
421
-
422
- @app_ui.post('/compare')
423
- async def compare(req: CompareRequest):
424
- try:
425
- def get_doc(did):
426
- if did < 0 or did >= len(df_sample): return None
427
- row = df_sample.iloc[did]
428
- return {
429
- 'doc_id' : int(did),
430
- 'product' : str(row[COL_PRODUCT]),
431
- 'review' : str(row[COL_REVIEW]),
432
- 'rating' : int(row[COL_RATE]),
433
- 'sentiment': str(row[COL_SENTIMENT]),
434
- 'price' : int(row[COL_PRICE]),
435
- 'summary' : str(row[COL_SUMMARY]),
436
- }
437
- a = get_doc(req.doc_id_a)
438
- b = get_doc(req.doc_id_b)
439
- if not a or not b:
440
- return {'error': 'Invalid doc IDs'}
441
- prompt = (f'Compare these two products based on customer reviews:\n'
442
- f'Product A: {a["product"]}\nReview A: {a["review"][:200]}\n'
443
- f'Product B: {b["product"]}\nReview B: {b["review"][:200]}\n'
444
- f'Which is better and why? Answer in 3 sentences:')
445
- inputs = _tok(prompt, return_tensors='pt', max_length=512, truncation=True)
446
- with torch.no_grad():
447
- out = _model.generate(input_ids=inputs['input_ids'], attention_mask=inputs['attention_mask'],
448
- max_new_tokens=120, num_beams=2, early_stopping=True, no_repeat_ngram_size=3)
449
- verdict = _tok.decode(out[0], skip_special_tokens=True).strip()
450
- return {'doc_a': a, 'doc_b': b, 'verdict': verdict}
451
- except Exception as e:
452
- return {'error': str(e)}
453
-
454
- @app_ui.get('/autocomplete')
455
- async def autocomplete(q: str = ''):
456
- if not q or len(q) < 2:
457
- return {'suggestions': []}
458
- ql = q.lower()
459
- matches = [w for w in _autocomplete_corpus if w.startswith(ql)][:8]
460
- return {'suggestions': matches}
461
 
462
  @app_ui.post('/analyse')
463
  async def analyse(file: UploadFile = File(...)):
464
  try:
465
- content = await file.read()
 
466
  fname = file.filename.lower()
467
- if fname.endswith('.csv'): udf = pd.read_csv(io.BytesIO(content))
468
- elif fname.endswith(('.xlsx','.xls')): udf = pd.read_excel(io.BytesIO(content))
469
- elif fname.endswith('.json'): udf = pd.read_json(io.BytesIO(content))
470
- else: return {'error': 'Use CSV, Excel, or JSON'}
471
- if len(udf) == 0: return {'error': 'File is empty'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  report = {}
473
  report['rows'] = int(len(udf))
474
  report['columns'] = list(udf.columns)
475
  report['missing'] = {c: int(udf[c].isna().sum()) for c in udf.columns}
476
- text_col = next((c for c in ['review','Review','text','Text','comment','Comment','description','feedback'] if c in udf.columns), None)
 
 
477
  if text_col is None:
478
  obj = udf.select_dtypes(include='object').columns
479
  text_col = obj[0] if len(obj) > 0 else None
480
- rating_col = next((c for c in ['rating','Rating','rate','Rate','stars','score'] if c in udf.columns), None)
481
- sent_col = next((c for c in ['sentiment','Sentiment','label','Label'] if c in udf.columns), None)
 
 
482
  report['detected'] = {'text_column': text_col, 'rating_column': rating_col, 'sentiment_column': sent_col}
483
  if text_col:
484
  lengths = udf[text_col].dropna().astype(str).apply(len)
485
  words = udf[text_col].dropna().astype(str).apply(lambda x: len(x.split()))
486
- report['text_stats'] = {'avg_length': round(float(lengths.mean()),1), 'max_length': int(lengths.max()),
487
- 'avg_words': round(float(words.mean()),1), 'total_non_empty': int((udf[text_col].notna() & (udf[text_col].astype(str).str.len()>0)).sum())}
488
- wf = Counter(w for w in re.sub(r'[^a-z\s]','', ' '.join(udf[text_col].dropna().astype(str).str.lower().tolist())).split()
 
 
 
 
 
 
 
489
  if w not in STOPWORDS and len(w) > 2)
490
- report['top_words'] = [{'word':w,'count':c} for w,c in wf.most_common(15)]
491
  else:
492
- report['text_stats'] = {}; report['top_words'] = []
 
493
  if rating_col:
494
  rv = pd.to_numeric(udf[rating_col], errors='coerce').dropna()
495
- report['rating_stats'] = {'avg': round(float(rv.mean()),2),
496
- 'distribution': {str(int(k)):int(v) for k,v in rv.value_counts().sort_index().items()}}
497
- else: report['rating_stats'] = {}
 
 
 
498
  if sent_col:
499
  sv = udf[sent_col].dropna().astype(str).str.lower().str.strip()
500
- report['sentiment_stats'] = {str(k):int(v) for k,v in sv.value_counts().items()}
501
- else: report['sentiment_stats'] = {}
 
502
  if text_col:
503
  sample_text = ' '.join(udf[text_col].dropna().astype(str).head(8).tolist())[:800]
504
- prompt = f'Analyse these reviews and summarise key themes and insights:\n{sample_text}\nSummary:'
 
 
 
 
 
505
  inputs = _tok(prompt, return_tensors='pt', max_length=512, truncation=True)
506
  with torch.no_grad():
507
- out = _model.generate(input_ids=inputs['input_ids'], attention_mask=inputs['attention_mask'],
508
- max_new_tokens=120, num_beams=2, early_stopping=True, no_repeat_ngram_size=3)
 
 
 
 
509
  report['ai_summary'] = _tok.decode(out[0], skip_special_tokens=True).strip()
510
  else:
511
  report['ai_summary'] = 'No text column detected.'
512
- return {'success': True, 'filename': file.filename, 'report': report}
 
 
513
  except Exception as e:
514
- return {'success': False, 'error': str(e)}
 
515
 
516
  @app_ui.get('/stats')
517
  async def stats():
@@ -1242,15 +1090,6 @@ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').repl
1242
  </html>"""
1243
 
1244
 
1245
- @app_ui.get('/', response_class=HTMLResponse)
1246
- async def root():
1247
- return HTML_UI
1248
-
1249
-
1250
- @app_ui.get('/', response_class=HTMLResponse)
1251
- async def root():
1252
- return HTML_UI
1253
-
1254
 
1255
  if __name__ == '__main__':
1256
  uvicorn.run(app_ui, host='0.0.0.0', port=7860)
 
9
  from pydantic import BaseModel
10
  from rank_bm25 import BM25Okapi
11
  from sentence_transformers import SentenceTransformer
12
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
13
  from collections import Counter
14
  from typing import Optional
15
 
 
28
  'would','could','should','its','if','about','than','then','too','s','t'
29
  }
30
 
 
 
 
31
 
32
  def preprocess(text, remove_stops=False):
33
  if not isinstance(text, str) or not text.strip(): return ''
 
41
  return text
42
 
43
 
 
44
  CSV_PATH = os.getenv('CSV_PATH', 'Dataset-SA.csv')
 
45
  print('Loading data...')
46
  df_raw = pd.read_csv(CSV_PATH)
47
  mask_bad = pd.to_numeric(df_raw[COL_RATE], errors='coerce').isna()
 
62
  df_sample = df.sample(n=min(1000, len(df)), random_state=42).reset_index(drop=True)
63
  print(f'Data: {len(df):,} rows | sample: {len(df_sample):,}')
64
 
65
+ print('Loading SBERT...')
66
  sbert = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
67
  EMBED_DIM = sbert.get_sentence_embedding_dimension()
68
+ embeddings = sbert.encode(df_sample['clean_text'].tolist(), batch_size=16,
69
+ show_progress_bar=True, convert_to_numpy=True, normalize_embeddings=False)
70
+ print('Building FAISS...')
 
 
 
71
  emb_norm = embeddings.copy().astype('float32')
72
  faiss.normalize_L2(emb_norm)
73
  index_flat = faiss.IndexFlatIP(EMBED_DIM)
74
  index_flat.add(emb_norm)
 
75
  print('Building BM25...')
76
  tokenized_corpus = [doc.split() for doc in df_sample['clean_text_bm25'].tolist()]
77
  bm25 = BM25Okapi(tokenized_corpus)
 
78
  print('Loading Flan-T5...')
79
+ _tok = AutoTokenizer.from_pretrained('google/flan-t5-base')
80
+ _model = AutoModelForSeq2SeqLM.from_pretrained('google/flan-t5-base')
81
  _model.eval()
82
+ print('Building autocomplete...')
 
83
  _all_words = Counter()
84
+ for txt in df_sample['clean_text_bm25'].sample(min(1000, len(df_sample))):
85
  _all_words.update(txt.split())
86
  _top_words = [w for w, c in _all_words.most_common(500) if len(w) > 3]
87
  _product_names = df_sample[COL_PRODUCT].str.lower().str[:50].drop_duplicates().head(200).tolist()
88
  _autocomplete_corpus = list(set(_top_words + _product_names))
89
+ print(f'All models ready!')
90
+
91
 
92
  # ── Search cache (for performance) ────────────────────────────
93
  _cache = {}
 
173
  if len(rows) == k: break
174
  return rows
175
 
 
 
 
 
 
 
 
 
 
 
176
  # ── FastAPI ────────────────────────────────────────────────────
177
  app_ui = FastAPI(title='FlipSearch Ultimate')
178
  app_ui.add_middleware(CORSMiddleware, allow_origins=['*'], allow_methods=['*'], allow_headers=['*'])
 
261
  matches = [w for w in _autocomplete_corpus if w.startswith(ql)][:8]
262
  return {'suggestions': matches}
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
  @app_ui.post('/analyse')
266
  async def analyse(file: UploadFile = File(...)):
267
  try:
268
+ MAX_SIZE_MB = 50
269
+ ALLOWED_TYPES = {'.csv', '.xlsx', '.xls', '.json'}
270
  fname = file.filename.lower()
271
+ ext = os.path.splitext(fname)[1]
272
+ if ext not in ALLOWED_TYPES:
273
+ return {'success': False, 'error': 'File type not allowed. Use CSV, Excel or JSON.'}
274
+ content = await file.read()
275
+ if len(content) > MAX_SIZE_MB * 1024 * 1024:
276
+ return {'success': False, 'error': f'File too large. Max {MAX_SIZE_MB}MB.'}
277
+ if len(content) == 0:
278
+ return {'success': False, 'error': 'File is empty.'}
279
+ if ext == '.csv':
280
+ udf = pd.read_csv(io.BytesIO(content))
281
+ elif ext in ('.xlsx', '.xls'):
282
+ udf = pd.read_excel(io.BytesIO(content))
283
+ else:
284
+ udf = pd.read_json(io.BytesIO(content))
285
+ if len(udf) == 0:
286
+ return {'success': False, 'error': 'No data rows found.'}
287
+ if len(udf) > 500000:
288
+ return {'success': False, 'error': 'Too many rows. Max 500,000.'}
289
+ udf.columns = [str(c)[:50] for c in udf.columns]
290
+ for col in udf.select_dtypes(include='object').columns:
291
+ udf[col] = udf[col].astype(str).str[:1000]
292
  report = {}
293
  report['rows'] = int(len(udf))
294
  report['columns'] = list(udf.columns)
295
  report['missing'] = {c: int(udf[c].isna().sum()) for c in udf.columns}
296
+ text_col = next((c for c in ['review','Review','text','Text','comment',
297
+ 'Comment','description','feedback','body']
298
+ if c in udf.columns), None)
299
  if text_col is None:
300
  obj = udf.select_dtypes(include='object').columns
301
  text_col = obj[0] if len(obj) > 0 else None
302
+ rating_col = next((c for c in ['rating','Rating','rate','Rate','stars','score','Score']
303
+ if c in udf.columns), None)
304
+ sent_col = next((c for c in ['sentiment','Sentiment','label','Label']
305
+ if c in udf.columns), None)
306
  report['detected'] = {'text_column': text_col, 'rating_column': rating_col, 'sentiment_column': sent_col}
307
  if text_col:
308
  lengths = udf[text_col].dropna().astype(str).apply(len)
309
  words = udf[text_col].dropna().astype(str).apply(lambda x: len(x.split()))
310
+ report['text_stats'] = {
311
+ 'avg_length' : round(float(lengths.mean()), 1),
312
+ 'max_length' : int(lengths.max()),
313
+ 'min_length' : int(lengths.min()),
314
+ 'avg_words' : round(float(words.mean()), 1),
315
+ 'total_non_empty': int((udf[text_col].notna() &
316
+ (udf[text_col].astype(str).str.len() > 0)).sum())
317
+ }
318
+ all_words = ' '.join(udf[text_col].dropna().astype(str).str.lower().tolist())
319
+ wf = Counter(w for w in re.sub(r'[^a-z\s]', '', all_words).split()
320
  if w not in STOPWORDS and len(w) > 2)
321
+ report['top_words'] = [{'word': w, 'count': c} for w, c in wf.most_common(15)]
322
  else:
323
+ report['text_stats'] = {}
324
+ report['top_words'] = []
325
  if rating_col:
326
  rv = pd.to_numeric(udf[rating_col], errors='coerce').dropna()
327
+ report['rating_stats'] = {
328
+ 'avg': round(float(rv.mean()), 2),
329
+ 'distribution': {str(int(k)): int(v) for k, v in rv.value_counts().sort_index().items()}
330
+ }
331
+ else:
332
+ report['rating_stats'] = {}
333
  if sent_col:
334
  sv = udf[sent_col].dropna().astype(str).str.lower().str.strip()
335
+ report['sentiment_stats'] = {str(k): int(v) for k, v in sv.value_counts().items()}
336
+ else:
337
+ report['sentiment_stats'] = {}
338
  if text_col:
339
  sample_text = ' '.join(udf[text_col].dropna().astype(str).head(8).tolist())[:800]
340
+ prompt = (
341
+ 'You are a data analyst. Read these customer reviews carefully.\n'
342
+ f'Reviews: {sample_text}\n\n'
343
+ 'Summarise: (1) overall satisfaction (2) common praise '
344
+ '(3) common complaints (4) product quality.\nAnswer:'
345
+ )
346
  inputs = _tok(prompt, return_tensors='pt', max_length=512, truncation=True)
347
  with torch.no_grad():
348
+ out = _model.generate(
349
+ input_ids=inputs['input_ids'],
350
+ attention_mask=inputs['attention_mask'],
351
+ max_new_tokens=120, num_beams=2,
352
+ early_stopping=True, no_repeat_ngram_size=3
353
+ )
354
  report['ai_summary'] = _tok.decode(out[0], skip_special_tokens=True).strip()
355
  else:
356
  report['ai_summary'] = 'No text column detected.'
357
+ return {'success': True, 'filename': file.filename[:100], 'report': report}
358
+ except pd.errors.ParserError:
359
+ return {'success': False, 'error': 'Could not parse file. Check it is valid CSV/Excel/JSON.'}
360
  except Exception as e:
361
+ return {'success': False, 'error': f'Analysis failed: {str(e)[:300]}'}
362
+
363
 
364
  @app_ui.get('/stats')
365
  async def stats():
 
1090
  </html>"""
1091
 
1092
 
 
 
 
 
 
 
 
 
 
1093
 
1094
  if __name__ == '__main__':
1095
  uvicorn.run(app_ui, host='0.0.0.0', port=7860)