RathodHarish commited on
Commit
1633032
·
verified ·
1 Parent(s): a3fd656

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +681 -392
app.py CHANGED
@@ -1,440 +1,729 @@
1
  import gradio as gr
2
- import librosa
3
- import numpy as np
4
- import torch
5
- from transformers import WhisperProcessor, WhisperForConditionalGeneration
6
- from simple_salesforce import Salesforce
7
- import os
8
- from datetime import datetime
9
  import logging
10
- import webrtcvad
11
- import google.generativeai as genai
12
- from gtts import gTTS
13
- import tempfile
14
- import base64
15
- import re
16
- import subprocess
17
- from cryptography.fernet import Fernet
18
-
19
- # Set up logging
20
- logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
21
- logger = logging.getLogger(__name__)
22
- usage_metrics = {"total_assessments": 0, "assessments_by_language": {}}
23
-
24
- # Environment variables
25
- SF_USERNAME = os.getenv("SF_USERNAME", "smartvoicebot@voice.com")
26
- SF_PASSWORD = os.getenv("SF_PASSWORD", "voicebot1")
27
- SF_SECURITY_TOKEN = os.getenv("SF_SECURITY_TOKEN", "jq4VVHUFti6TmzJDjjegv2h6b")
28
- SF_INSTANCE_URL = os.getenv("SF_INSTANCE_URL", "https://swe42.sfdc-cehfhs.salesforce.com")
29
- GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "AIzaSyBzr5vVpbe8CV1v70l3pGDp9vRJ76yCxdk")
30
- ENCRYPTION_KEY = os.getenv("ENCRYPTION_KEY", Fernet.generate_key().decode())
31
- DEFAULT_EMAIL = os.getenv("SALESFORCE_USER_EMAIL", "default@mindcare.com")
32
-
33
- # Initialize encryption
34
- cipher = Fernet(ENCRYPTION_KEY)
35
-
36
- # Initialize Salesforce
37
  try:
38
  sf = Salesforce(
39
- username=SF_USERNAME,
40
- password=SF_PASSWORD,
41
- security_token=SF_SECURITY_TOKEN,
42
- instance_url=SF_INSTANCE_URL
43
  )
44
- logger.info(f"Connected to Salesforce at {SF_INSTANCE_URL}")
45
  except Exception as e:
46
- logger.error(f"Salesforce connection failed: {str(e)}")
47
  sf = None
48
 
49
- # Initialize Google Gemini
50
  try:
51
- genai.configure(api_key=GEMINI_API_KEY)
52
- gemini_model = genai.GenerativeModel('gemini-1.5-flash')
53
- chat = gemini_model.start_chat(history=[])
54
- logger.info("Connected to Google Gemini")
55
- except Exception as e:
56
- logger.error(f"Google Gemini initialization failed: {str(e)}")
57
- chat = None
58
-
59
- # Load Whisper model
60
- SUPPORTED_LANGUAGES = {"en": "english", "es": "spanish", "hi": "hindi", "zh": "mandarin"}
61
- SALESFORCE_LANGUAGE_MAP = {"en": "English", "es": "Spanish", "hi": "Hindi", "zh": "Mandarin"}
62
- whisper_processor = WhisperProcessor.from_pretrained("openai/whisper-small")
63
- whisper_model = WhisperForConditionalGeneration.from_pretrained("openai/whisper-small")
64
- vad = webrtcvad.Vad(mode=2)
65
-
66
- # Context for chatbot
67
- base_info = """
68
- You are MindCare, an AI health assistant providing support in:
69
- - Mental health: Emotional support, stress management
70
- - Medical guidance: Symptom analysis, general advice
71
- - General health: Lifestyle and wellness recommendations
72
- Tone: Empathetic, supportive, informative. Always suggest professional consultation for medical issues.
73
- """
74
- context = [base_info]
75
-
76
- def encrypt_data(data):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  try:
78
- return cipher.encrypt(data.encode('utf-8')).decode('utf-8')
 
 
 
 
 
 
 
 
79
  except Exception as e:
80
- logger.error(f"Encryption failed: {str(e)}")
81
- return data
 
 
82
 
83
- def decrypt_data(encrypted_data):
 
 
 
 
84
  try:
85
- return cipher.decrypt(encrypted_data.encode('utf-8')).decode('utf-8')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  except Exception as e:
87
- logger.error(f"Decryption failed: {str(e)}")
88
- return encrypted_data
89
 
90
- def extract_health_features(audio, sr):
 
 
 
 
91
  try:
92
- audio = audio / np.max(np.abs(audio)) if np.max(np.abs(audio)) != 0 else audio
93
- frame_duration = 30
94
- frame_samples = int(sr * frame_duration / 1000)
95
- frames = [audio[i:i + frame_samples] for i in range(0, len(audio), frame_samples)]
96
- voiced_frames = [frame for frame in frames if len(frame) == frame_samples and vad.is_speech((frame * 32768).astype(np.int16).tobytes(), sr)]
97
- if not voiced_frames:
98
- raise ValueError("No voiced segments detected")
99
- voiced_audio = np.concatenate(voiced_frames)
100
-
101
- # Enhanced feature extraction
102
- pitches, magnitudes = librosa.piptrack(y=voiced_audio, sr=sr, fmin=75, fmax=300)
103
- valid_pitches = [p for p in pitches[magnitudes > 0] if 75 <= p <= 300]
104
- pitch = np.mean(valid_pitches) if valid_pitches else 0
105
- jitter = np.std(valid_pitches) / pitch if pitch and valid_pitches else 0
106
- jitter = min(jitter, 10) # Cap jitter
107
- amplitudes = librosa.feature.rms(y=voiced_audio, frame_length=2048, hop_length=512)[0]
108
- shimmer = np.std(amplitudes) / np.mean(amplitudes) if np.mean(amplitudes) else 0
109
- shimmer = min(shimmer, 10) # Cap shimmer
110
- energy = np.mean(librosa.feature.rms(y=voiced_audio, frame_length=2048, hop_length=512)[0])
111
-
112
- # Additional features
113
- mfcc = np.mean(librosa.feature.mfcc(y=voiced_audio, sr=sr, n_mfcc=13), axis=1)
114
- spectral_centroid = np.mean(librosa.feature.spectral_centroid(y=voiced_audio, sr=sr))
115
-
116
- return {
117
- "pitch": pitch,
118
- "jitter": jitter * 100,
119
- "shimmer": shimmer * 100,
120
- "energy": energy,
121
- "mfcc_mean": np.mean(mfcc),
122
- "spectral_centroid": spectral_centroid
123
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  except Exception as e:
125
- logger.error(f"Feature extraction failed: {str(e)}")
126
- raise
127
 
128
- def transcribe_audio(audio, language="en"):
 
129
  try:
130
- whisper_model.config.forced_decoder_ids = whisper_processor.get_decoder_prompt_ids(
131
- language=SUPPORTED_LANGUAGES.get(language, "english"), task="transcribe"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  )
133
- inputs = whisper_processor(audio, sampling_rate=16000, return_tensors="pt")
134
- with torch.no_grad():
135
- generated_ids = whisper_model.generate(inputs["input_features"])
136
- transcription = whisper_processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
137
- logger.info(f"Transcription (language: {language}): {transcription}")
138
- return transcription
139
  except Exception as e:
140
- logger.error(f"Transcription failed: {str(e)}")
141
- return None
142
 
143
- def get_chatbot_response(message, language="en"):
144
- if not chat or not message:
145
- return "Unable to generate response.", None
146
- full_context = "\n".join(context) + f"\nUser: {message}\nMindCare:"
147
  try:
148
- response = chat.send_message(full_context).text
149
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_audio:
150
- tts = gTTS(text=response, lang=language, slow=False)
151
- tts.save(temp_audio.name)
152
- audio_path = temp_audio.name
153
- return response, audio_path
 
 
 
 
 
 
 
 
 
154
  except Exception as e:
155
- logger.error(f"Chatbot response failed: {str(e)}")
156
- return "Error generating response.", None
157
-
158
- def analyze_symptoms(text, features):
159
- feedback = []
160
- text = text.lower() if text else ""
161
-
162
- # Voice-based health assessment
163
- if features["jitter"] > 2.0:
164
- feedback.append(f"Elevated jitter ({features['jitter']:.2f}%) detected, which may indicate respiratory strain or vocal cord issues. Consult a doctor.")
165
- if features["shimmer"] > 3.0:
166
- feedback.append(f"High shimmer ({features['shimmer']:.2f}%) suggests possible emotional stress or vocal fatigue. Consider professional evaluation.")
167
- if features["energy"] < 0.01:
168
- feedback.append(f"Low vocal energy ({features['energy']:.4f}) detected, which might indicate fatigue or low mood. Rest and medical advice recommended.")
169
- if features["pitch"] < 100 or features["pitch"] > 250:
170
- feedback.append(f"Unusual pitch ({features['pitch']:.2f} Hz) may indicate vocal cord issues or emotional stress.")
171
- if features["spectral_centroid"] > 2000:
172
- feedback.append(f"High spectral centroid ({features['spectral_centroid']:.2f} Hz) suggests tense speech, possibly linked to stress or anxiety.")
173
-
174
- # Text-based symptom analysis
175
- if "cough" in text or "breath" in text:
176
- feedback.append("Your description suggests respiratory symptoms. Possible conditions include bronchitis or asthma. Please consult a doctor.")
177
- if "stress" in text or "anxious" in text:
178
- feedback.append("You mentioned stress or anxiety. Try deep breathing or mindfulness. Consider speaking with a mental health professional.")
179
- if "pain" in text:
180
- feedback.append("Pain reported. For mild pain, consider Paracetamol; for inflammation, Ibuprofen may help. Consult a doctor before taking medication.")
181
- if not feedback:
182
- feedback.append("No specific health concerns detected from voice or text. Maintain a healthy lifestyle and consult a doctor if symptoms arise.")
183
-
184
- return "\n".join(feedback)
185
-
186
- def store_user_consent(language):
187
- if not sf:
188
- logger.warning("Salesforce not connected; skipping consent storage")
189
- return None
190
  try:
191
- user = sf.query(f"SELECT Id FROM HealthUser__c WHERE Email__c = '{DEFAULT_EMAIL}'")
192
- user_id = None
193
- if user["totalSize"] == 0:
194
- user = sf.HealthUser__c.create({
195
- "Email__c": DEFAULT_EMAIL,
196
- "Language__c": SALESFORCE_LANGUAGE_MAP.get(language, "English"),
197
- "ConsentGiven__c": True
198
- })
199
- user_id = user["id"]
200
- logger.info(f"Created new user with email: {DEFAULT_EMAIL}")
201
- else:
202
- user_id = user["records"][0]["Id"]
203
- sf.HealthUser__c.update(user_id, {
204
- "Language__c": SALESFORCE_LANGUAGE_MAP.get(language, "English"),
205
- "ConsentGiven__c": True
206
- })
207
- logger.info(f"Updated user with email: {DEFAULT_EMAIL}")
208
- sf.ConsentLog__c.create({
209
- "HealthUser__c": user_id,
210
- "ConsentType__c": "Voice Analysis",
211
- "ConsentDate__c": datetime.utcnow().isoformat()
212
- })
213
- return user_id
214
  except Exception as e:
215
- logger.error(f"Consent storage failed: {str(e)}")
216
- return None
217
 
218
- def generate_pdf_report(feedback, transcription, features, language):
 
219
  try:
220
- # Sanitize inputs for LaTeX
221
- feedback = feedback.replace('&', '\\&').replace('%', '\\%').replace('$', '\\$').replace('#', '\\#')
222
- transcription = transcription.replace('&', '\\&').replace('%', '\\%').replace('$', '\\$').replace('#', '\\#') if transcription else "None"
223
- email = DEFAULT_EMAIL.replace('&', '\\&').replace('%', '\\%').replace('$', '\\$').replace('#', '\\#')
224
- language_display = SALESFORCE_LANGUAGE_MAP.get(language, "English")
225
-
226
- latex_content = (
227
- "\\documentclass[a4paper,12pt]{article}\n"
228
- "\\usepackage[utf8]{inputenc}\n"
229
- "\\usepackage{geometry}\n"
230
- "\\usepackage{parskip}\n"
231
- "\\usepackage{titlesec}\n"
232
- "\\usepackage{times}\n"
233
- "\\usepackage{datetime}\n"
234
- "\\newdateformat{isodate}{\\THEDAY{} \\shortmonthname[\\THEMONTH] \\THEYEAR}\n"
235
- "\\geometry{margin=1in}\n"
236
- "\\titleformat{\\section}{\\large\\bfseries}{\\thesection}{1em}{}\n"
237
- "\\titleformat{\\subsection}{\\bfseries}{\\thesubsection}{1em}{}\n"
238
- "\\begin{document}\n"
239
- "\\begin{center}\n"
240
- " \\textbf{\\large MindCare Health Assistant Report} \\\\\n"
241
- " \\vspace{0.5cm}\n"
242
- " Generated on \\isodate\\today\\ at \\currenttime\n"
243
- "\\end{center}\n"
244
- "\\section*{User Information}\n"
245
- "\\begin{itemize}\n"
246
- f" \\item \\textbf{{Email}}: {email}\n"
247
- f" \\item \\textbf{{Language}}: {language_display}\n"
248
- "\\end{itemize}\n"
249
- "\\section*{Voice Analysis Results}\n"
250
- "\\subsection*{Health Assessment}\n"
251
- f"{feedback}\n"
252
- "\\subsection*{Transcription}\n"
253
- f"{transcription}\n"
254
- "\\subsection*{Voice Metrics}\n"
255
- "\\begin{itemize}\n"
256
- f" \\item \\textbf{{Pitch}}: {features['pitch']:.2f} Hz\n"
257
- f" \\item \\textbf{{Jitter}}: {features['jitter']:.2f}\\%\n"
258
- f" \\item \\textbf{{Shimmer}}: {features['shimmer']:.2f}\\%\n"
259
- f" \\item \\textbf{{Energy}}: {features['energy']:.4f}\n"
260
- f" \\item \\textbf{{MFCC Mean}}: {features['mfcc_mean']:.2f}\n"
261
- f" \\item \\textbf{{Spectral Centroid}}: {features['spectral_centroid']:.2f} Hz\n"
262
- "\\end{itemize}\n"
263
- "\\section*{Disclaimer}\n"
264
- "This report is a preliminary analysis and not a medical diagnosis. Always consult a healthcare provider.\n"
265
- "\\end{document}\n"
266
  )
267
-
268
- with tempfile.NamedTemporaryFile(delete=False, suffix=".tex") as tex_file:
269
- tex_file.write(latex_content.encode('utf-8'))
270
- tex_file_path = tex_file.name
271
- pdf_path = tex_file_path.replace('.tex', '.pdf')
272
- result = subprocess.run(
273
- ['latexmk', '-pdf', '-pdflatex=pdflatex', '-interaction=nonstopmode', tex_file_path],
274
- capture_output=True, text=True, check=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  )
276
- logger.info(f"PDF generation output: {result.stdout}")
277
- for ext in ['.aux', '.log', '.out', '.fls', '.fdb_latexmk']:
278
- try:
279
- os.remove(tex_file_path.replace('.tex', ext))
280
- except:
281
- pass
282
- if os.path.exists(pdf_path):
283
- logger.info(f"Generated PDF report: {pdf_path}")
284
- return pdf_path
285
- else:
286
- logger.error("PDF file was not created")
287
- return None
288
- except subprocess.CalledProcessError as e:
289
- logger.error(f"PDF generation failed: {e.stderr}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  except Exception as e:
292
- logger.error(f"PDF generation failed: { five(str(e))}")
293
  return None
294
 
295
- def store_in_salesforce(user_id, audio_file, feedback, respiratory_score, mental_health_score, features, transcription, language):
296
- if not sf:
297
- logger.warning("Salesforce not connected; skipping storage")
298
- return
299
  try:
300
- with open(audio_file, "rb") as f:
301
- audio_content = base64.b64encode(f.read()).decode()
302
- content_version = sf.ContentVersion.create({
303
- "Title": f"Voice_Assessment_{datetime.utcnow().isoformat()}",
304
- "PathOnClient": os.path.basename(audio_file),
305
- "VersionData": audio_content,
306
- "IsMajorVersion": True
307
- })
308
- content_document_id = sf.query(f"SELECT ContentDocumentId FROM ContentVersion WHERE Id = '{content_version['id']}'")["records"][0]["ContentDocumentId"]
309
- file_url = f"{SF_INSTANCE_URL}/lightning/r/ContentDocument/{content_document_id}/view"
310
-
311
- feedback_str = feedback.encode('utf-8').decode('utf-8')
312
- encrypted_feedback = encrypt_data(feedback_str)
313
- if len(encrypted_feedback) > 131072:
314
- encrypted_feedback = encrypted_feedback[:131072]
315
-
316
- assessment = sf.VoiceAssessment__c.create({
317
- "HealthUser__c": user_id,
318
- "VoiceRecording__c": file_url,
319
- "AssessmentResult__c": encrypted_feedback,
320
- "AssessmentDate__c": datetime.utcnow().isoformat(),
321
- "ConfidenceScore__c": 95.0,
322
- "RespiratoryScore__c": float(respiratory_score),
323
- "MentalHealthScore__c": float(mental_health_score),
324
- "Pitch__c": float(features["pitch"]),
325
- "Jitter__c": float(features["jitter"]),
326
- "Shimmer__c": float(features["shimmer"]),
327
- "Energy__c": float(features["energy"]),
328
- "Transcription__c": transcription or "None",
329
- "Language__c": SALESFORCE_LANGUAGE_MAP.get(language, "English")
330
- })
331
- sf.ContentDocumentLink.create({
332
- "ContentDocumentId": content_document_id,
333
- "LinkedEntityId": assessment["id"],
334
- "ShareType": "V"
335
- })
336
- logger.info(f"Stored assessment in Salesforce: {assessment['id']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  except Exception as e:
338
- logger.error(f"Salesforce storage failed: {str(e)}")
339
- raise
340
 
341
- def analyze_voice(audio_file=None, language="en"):
342
- global usage_metrics
343
- usage_metrics["total_assessments"] += 1
344
- usage_metrics["assessments_by_language"][language] = usage_metrics["assessments_by_language"].get(language, 0) + 1
 
 
 
 
 
345
 
 
 
 
 
346
  try:
347
- if not audio_file or not os.path.exists(audio_file):
348
- raise ValueError("No valid audio file provided")
349
-
350
- audio, sr = librosa.load(audio_file, sr=16000)
351
- if len(audio) < sr:
352
- raise ValueError("Audio too short (minimum 1 second)")
353
-
354
- user_id = store_user_consent(language)
355
- if not user_id:
356
- return "Error: Failed to store user consent.", None
357
-
358
- features = extract_health_features(audio, sr)
359
- transcription = transcribe_audio(audio, language)
360
- feedback = analyze_symptoms(transcription, features)
361
-
362
- respiratory_score = features["jitter"]
363
- mental_health_score = features["shimmer"]
364
-
365
- feedback += f"\n\n**Voice Analysis Details**:\n"
366
- feedback += f"- Pitch: {features['pitch']:.2f} Hz\n"
367
- feedback += f"- Jitter: {features['jitter']:.2f}% (voice stability)\n"
368
- feedback += f"- Shimmer: {features['shimmer']:.2f}% (amplitude variation)\n"
369
- feedback += f"- Energy: {features['energy']:.4f} (vocal intensity)\n"
370
- feedback += f"- MFCC Mean: {features['mfcc_mean']:.2f} (timbre quality)\n"
371
- feedback += f"- Spectral Centroid: {features['spectral_centroid']:.2f} Hz (voice brightness)\n"
372
- feedback += f"- Transcription: {transcription if transcription else 'None'}\n"
373
- feedback += "\n**Disclaimer**: This is a preliminary analysis. Consult a healthcare provider for professional evaluation."
374
-
375
- if sf:
376
- store_in_salesforce(user_id, audio_file, feedback, respiratory_score, mental_health_score, features, transcription, language)
377
-
378
- pdf_path = generate_pdf_report(feedback, transcription, features, language)
379
-
380
- try:
381
- os.remove(audio_file)
382
- logger.info(f"Deleted audio file: {audio_file}")
383
- except Exception as e:
384
- logger.error(f"Failed to delete audio file: {str(e)}")
385
-
386
- return feedback, pdf_path
387
  except Exception as e:
388
- logger.error(f"Audio processing failed: {str(e)}")
389
- return f"Error: {str(e)}", None
390
 
391
- def launch():
392
- with gr.Blocks(title="MindCare Health Assistant", css=".gradio-container {max-width: 1200px; margin: auto; font-family: Arial, sans-serif;}") as demo:
393
- gr.Markdown("# MindCare Health Assistant")
394
- gr.Markdown("Record your voice or type a message for health assessments and suggestions.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
396
  with gr.Row():
397
- with gr.Column():
398
- gr.Markdown("### Voice Analysis")
399
- gr.Markdown("Record or upload voice (1+ sec) describing symptoms (e.g., 'I have a cough' or 'I feel stressed').")
400
- language_input = gr.Dropdown(choices=list(SUPPORTED_LANGUAGES.keys()), label="Select Language", value="en")
401
- consent_input = gr.Checkbox(label="I consent to data storage and voice analysis", value=True, interactive=False)
402
- audio_input = gr.Audio(type="filepath", label="Record or Upload Voice (WAV, MP3, FLAC)", format="wav")
403
- voice_output = gr.Textbox(label="Health Assessment Results", elem_id="health-results")
404
- pdf_output = gr.File(label="Download Assessment Report (PDF)")
405
- submit_btn = gr.Button("Submit")
406
- clear_btn = gr.Button("Clear")
407
-
408
- with gr.Column():
409
- gr.Markdown("### Health Suggestions")
410
- gr.Markdown("Enter a message for personalized health advice.")
411
- text_input = gr.Textbox(label="Enter your message")
412
- text_output = gr.Textbox(label="Response")
413
- audio_output = gr.Audio(label="Response Audio")
414
- suggest_submit_btn = gr.Button("Submit")
415
- suggest_clear_btn = gr.Button("Clear")
416
-
417
- submit_btn.click(
418
- fn=analyze_voice,
419
- inputs=[audio_input, language_input],
420
- outputs=[voice_output, pdf_output]
421
- )
422
- clear_btn.click(
423
- fn=lambda: (gr.update(value=None), gr.update(value="en"), gr.update(value=""), gr.update(value=None)),
424
- inputs=None,
425
- outputs=[audio_input, language_input, voice_output, pdf_output]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  )
427
- suggest_submit_btn.click(
428
- fn=get_chatbot_response,
429
- inputs=[text_input, language_input],
430
- outputs=[text_output, audio_output]
 
431
  )
432
- suggest_clear_btn.click(
433
- fn=lambda: (gr.update(value=""), gr.update(value=""), gr.update(value=None)),
434
- inputs=None,
435
- outputs=[text_input, text_output, audio_output]
 
436
  )
437
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
438
 
439
  if __name__ == "__main__":
440
- launch()
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
+ import pandas as pd
3
+ from datetime import datetime, timedelta
 
 
 
 
 
4
  import logging
5
+ import plotly.express as px
6
+ import plotly.graph_objects as go
7
+ from sklearn.ensemble import IsolationForest
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ import os
10
+ import io
11
+ import time
12
+ import asyncio
13
+ from simple_salesforce import Salesforce
14
+
15
+ # Configure logging
16
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
17
+
18
+ # Salesforce configuration
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  try:
20
  sf = Salesforce(
21
+ username='multi-devicelabopsdashboard@sathkrutha.com',
22
+ password='Team@1234',
23
+ security_token=os.getenv('SF_SECURITY_TOKEN', ''),
24
+ domain='login'
25
  )
26
+ logging.info("Salesforce connection established")
27
  except Exception as e:
28
+ logging.error(f"Failed to connect to Salesforce: {str(e)}")
29
  sf = None
30
 
31
+ # Try to import reportlab
32
  try:
33
+ from reportlab.lib.pagesizes import letter
34
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
35
+ from reportlab.lib.styles import getSampleStyleSheet
36
+ from reportlab.lib import colors
37
+ reportlab_available = True
38
+ logging.info("reportlab module successfully imported")
39
+ except ImportError:
40
+ logging.warning("reportlab module not found. PDF generation disabled.")
41
+ reportlab_available = False
42
+
43
+ # Cache picklist values at startup
44
+ def get_picklist_values(field_name):
45
+ if sf is None:
46
+ return []
47
+ try:
48
+ obj_desc = sf.SmartLog__c.describe()
49
+ for field in obj_desc['fields']:
50
+ if field['name'] == field_name:
51
+ return [value['value'] for value in field['picklistValues'] if value['active']]
52
+ return []
53
+ except Exception as e:
54
+ logging.error(f"Failed to fetch picklist values for {field_name}: {str(e)}")
55
+ return []
56
+
57
+ status_values = get_picklist_values('Status__c') or ["Active", "Inactive", "Pending"]
58
+ log_type_values = get_picklist_values('Log_Type__c') or ["Smart Log", "Cell Analysis", "UV Verification"]
59
+ logging.info(f"Valid Status__c values: {status_values}")
60
+ logging.info(f"Valid Log_Type__c values: {log_type_values}")
61
+
62
+ # Map invalid picklist values
63
+ picklist_mapping = {
64
+ 'Status__c': {
65
+ 'normal': 'Active',
66
+ 'error': 'Inactive',
67
+ 'warning': 'Pending',
68
+ 'ok': 'Active',
69
+ 'failed': 'Inactive'
70
+ },
71
+ 'Log_Type__c': {
72
+ 'maint': 'Smart Log',
73
+ 'error': 'Cell Analysis',
74
+ 'ops': 'UV Verification',
75
+ 'maintenance': 'Smart Log',
76
+ 'cell': 'Cell Analysis',
77
+ 'uv': 'UV Verification',
78
+ 'weight log': 'Smart Log'
79
+ }
80
+ }
81
+
82
+ # Cache folder ID for Salesforce reports
83
+ def get_folder_id(folder_name):
84
+ if sf is None:
85
+ return None
86
  try:
87
+ query = f"SELECT Id FROM Folder WHERE Name = '{folder_name}' AND Type = 'Report'"
88
+ result = sf.query(query)
89
+ if result['totalSize'] > 0:
90
+ folder_id = result['records'][0]['Id']
91
+ logging.info(f"Found folder ID for '{folder_name}': {folder_id}")
92
+ return folder_id
93
+ else:
94
+ logging.error(f"Folder '{folder_name}' not found in Salesforce.")
95
+ return None
96
  except Exception as e:
97
+ logging.error(f"Failed to fetch folder ID for '{folder_name}': {str(e)}")
98
+ return None
99
+
100
+ LABOPS_REPORTS_FOLDER_ID = get_folder_id('LabOps Reports')
101
 
102
+ # Salesforce report creation
103
+ def create_salesforce_reports(df):
104
+ if sf is None or not LABOPS_REPORTS_FOLDER_ID:
105
+ logging.error("Cannot create Salesforce reports: No connection or folder ID")
106
+ return
107
  try:
108
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
109
+ reports = [
110
+ {
111
+ "reportMetadata": {
112
+ "name": f"SmartLog_Usage_Report_{timestamp}",
113
+ "developerName": f"SmartLog_Usage_Report_{timestamp}",
114
+ "reportType": {"type": "CustomEntity", "value": "SmartLog__c"},
115
+ "reportFormat": "TABULAR",
116
+ "reportBooleanFilter": None,
117
+ "reportFilters": [],
118
+ "detailColumns": ["SmartLog__c.Device_Id__c", "SmartLog__c.Usage_Hours__c"],
119
+ "folderId": LABOPS_REPORTS_FOLDER_ID
120
+ }
121
+ },
122
+ {
123
+ "reportMetadata": {
124
+ "name": f"SmartLog_AMC_Reminders_{timestamp}",
125
+ "developerName": f"SmartLog_AMC_Reminders_{timestamp}",
126
+ "reportType": {"type": "CustomEntity", "value": "SmartLog__c"},
127
+ "reportFormat": "TABULAR",
128
+ "reportBooleanFilter": None,
129
+ "reportFilters": [],
130
+ "detailColumns": ["SmartLog__c.Device_Id__c", "SmartLog__c.AMC_Date__c"],
131
+ "folderId": LABOPS_REPORTS_FOLDER_ID
132
+ }
133
+ }
134
+ ]
135
+ for report in reports:
136
+ sf.restful('analytics/reports', method='POST', json=report)
137
+ logging.info("Salesforce reports created successfully")
138
  except Exception as e:
139
+ logging.error(f"Failed to create Salesforce reports: {str(e)}")
 
140
 
141
+ # Save to Salesforce
142
+ def save_to_salesforce(df, reminders_df):
143
+ if sf is None:
144
+ logging.error("No Salesforce connection available")
145
+ return
146
  try:
147
+ logging.info("Starting Salesforce save operation")
148
+ current_date = datetime.now()
149
+ next_30_days = current_date + timedelta(days=30)
150
+ records = []
151
+ reminder_device_ids = set(reminders_df['device_id']) if not reminders_df.empty else set()
152
+ logging.info(f"Processing {len(df)} records for Salesforce")
153
+
154
+ for idx, row in df.iterrows():
155
+ status = str(row['status']).lower()
156
+ log_type = str(row['log_type']).lower()
157
+ status_mapped = picklist_mapping['Status__c'].get(status, status_values[0] if status_values else 'Active')
158
+ log_type_mapped = picklist_mapping['Log_Type__c'].get(log_type, log_type_values[0] if log_type_values else 'Smart Log')
159
+
160
+ if not status_mapped or not log_type_mapped:
161
+ logging.warning(f"Skipping record {idx}: Invalid status ({status}) or log_type ({log_type})")
162
+ continue
163
+
164
+ amc_date_str = None
165
+ if pd.notna(row['amc_date']):
166
+ try:
167
+ amc_date = pd.to_datetime(row['amc_date']).strftime('%Y-%m-%d')
168
+ amc_date_str = amc_date
169
+ amc_date_dt = datetime.strptime(amc_date, '%Y-%m-%d')
170
+ if status_mapped == "Active" and current_date.date() <= amc_date_dt.date() <= next_30_days.date():
171
+ logging.info(f"AMC Reminder for Device ID {row['device_id']}: {amc_date}")
172
+ except Exception as e:
173
+ logging.warning(f"Invalid AMC date for Device ID {row['device_id']}: {str(e)}")
174
+
175
+ record = {
176
+ 'Device_Id__c': str(row['device_id'])[:50],
177
+ 'Log_Type__c': log_type_mapped,
178
+ 'Status__c': status_mapped,
179
+ 'Timestamp__c': row['timestamp'].isoformat() if pd.notna(row['timestamp']) else None,
180
+ 'Usage_Hours__c': float(row['usage_hours']) if pd.notna(row['usage_hours']) else 0.0,
181
+ 'Downtime__c': float(row['downtime']) if pd.notna(row['downtime']) else 0.0,
182
+ 'AMC_Date__c': amc_date_str
183
+ }
184
+ records.append(record)
185
+
186
+ if records:
187
+ batch_size = 100
188
+ for i in range(0, len(records), batch_size):
189
+ batch = records[i:i + batch_size]
190
+ try:
191
+ result = sf.bulk.SmartLog__c.insert(batch)
192
+ logging.info(f"Saved {len(batch)} records to Salesforce in batch {i//batch_size + 1}")
193
+ for res in result:
194
+ if not res['success']:
195
+ logging.error(f"Failed to save record: {res['errors']}")
196
+ except Exception as e:
197
+ logging.error(f"Failed to save batch {i//batch_size + 1}: {str(e)}")
198
+ else:
199
+ logging.warning("No records to save to Salesforce")
200
  except Exception as e:
201
+ logging.error(f"Failed to save to Salesforce: {str(e)}")
 
202
 
203
+ # Summarize logs
204
+ def summarize_logs(df):
205
  try:
206
+ total_devices = df["device_id"].nunique()
207
+ total_usage = df["usage_hours"].sum() if "usage_hours" in df.columns else 0
208
+ return f"{total_devices} devices processed with {total_usage:.2f} total usage hours."
209
+ except Exception as e:
210
+ logging.error(f"Summary generation failed: {str(e)}")
211
+ return "Failed to generate summary."
212
+
213
+ # Anomaly detection
214
+ def detect_anomalies(df):
215
+ try:
216
+ if "usage_hours" not in df.columns or "downtime" not in df.columns:
217
+ return "Anomaly detection requires 'usage_hours' and 'downtime' columns.", pd.DataFrame()
218
+ features = df[["usage_hours", "downtime"]].fillna(0)
219
+ if len(features) > 50:
220
+ features = features.sample(n=50, random_state=42)
221
+ iso_forest = IsolationForest(contamination=0.1, random_state=42)
222
+ df["anomaly"] = iso_forest.fit_predict(features)
223
+ anomalies = df[df["anomaly"] == -1][["device_id", "usage_hours", "downtime", "timestamp"]]
224
+ if anomalies.empty:
225
+ return "No anomalies detected.", anomalies
226
+ return "\n".join([f"- Device ID: {row['device_id']}, Usage: {row['usage_hours']}, Downtime: {row['downtime']}, Timestamp: {row['timestamp']}" for _, row in anomalies.head(5).iterrows()]), anomalies
227
+ except Exception as e:
228
+ logging.error(f"Anomaly detection failed: {str(e)}")
229
+ return f"Anomaly detection failed: {str(e)}", pd.DataFrame()
230
+
231
+ # AMC reminders
232
+ def check_amc_reminders(df, current_date):
233
+ try:
234
+ if "device_id" not in df.columns or "amc_date" not in df.columns:
235
+ return "AMC reminders require 'device_id' and 'amc_date' columns.", pd.DataFrame()
236
+ df["amc_date"] = pd.to_datetime(df["amc_date"], errors='coerce')
237
+ current_date = pd.to_datetime(current_date)
238
+ df["days_to_amc"] = (df["amc_date"] - current_date).dt.days
239
+ reminders = df[(df["days_to_amc"] >= 0) & (df["days_to_amc"] <= 30)][["device_id", "log_type", "status", "timestamp", "usage_hours", "downtime", "amc_date"]]
240
+ if reminders.empty:
241
+ return "No AMC reminders due within the next 30 days.", reminders
242
+ return "\n".join([f"- Device ID: {row['device_id']}, AMC Date: {row['amc_date']}" for _, row in reminders.head(5).iterrows()]), reminders
243
+ except Exception as e:
244
+ logging.error(f"AMC reminder generation failed: {str(e)}")
245
+ return f"AMC reminder generation failed: {str(e)}", pd.DataFrame()
246
+
247
+ # Dashboard insights
248
+ def generate_dashboard_insights(df):
249
+ try:
250
+ total_devices = df["device_id"].nunique()
251
+ avg_usage = df["usage_hours"].mean() if "usage_hours" in df.columns else 0
252
+ return f"{total_devices} devices with average usage of {avg_usage:.2f} hours."
253
+ except Exception as e:
254
+ logging.error(f"Dashboard insights generation failed: {str(e)}")
255
+ return "Failed to generate insights."
256
+
257
+ # Placeholder chart for empty data
258
+ def create_placeholder_chart(title):
259
+ fig = go.Figure()
260
+ fig.add_annotation(
261
+ text="No data available for this chart",
262
+ xref="paper", yref="paper",
263
+ x=0.5, y=0.5, showarrow=False,
264
+ font=dict(size=16)
265
+ )
266
+ fig.update_layout(title=title, margin=dict(l=20, r=20, t=40, b=20))
267
+ return fig
268
+
269
+ # Create usage chart
270
+ def create_usage_chart(df):
271
+ try:
272
+ if df.empty or "usage_hours" not in df.columns or "device_id" not in df.columns:
273
+ logging.warning("Insufficient data for usage chart")
274
+ return create_placeholder_chart("Usage Hours per Device")
275
+ usage_data = df.groupby("device_id")["usage_hours"].sum().reset_index()
276
+ if len(usage_data) > 5:
277
+ usage_data = usage_data.nlargest(5, "usage_hours")
278
+ fig = px.bar(
279
+ usage_data,
280
+ x="device_id",
281
+ y="usage_hours",
282
+ title="Usage Hours per Device",
283
+ labels={"device_id": "Device ID", "usage_hours": "Usage Hours"}
284
  )
285
+ fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
286
+ return fig
 
 
 
 
287
  except Exception as e:
288
+ logging.error(f"Failed to create usage chart: {str(e)}")
289
+ return create_placeholder_chart("Usage Hours per Device")
290
 
291
+ # Create downtime chart
292
+ def create_downtime_chart(df):
 
 
293
  try:
294
+ if df.empty or "downtime" not in df.columns or "device_id" not in df.columns:
295
+ logging.warning("Insufficient data for downtime chart")
296
+ return create_placeholder_chart("Downtime per Device")
297
+ downtime_data = df.groupby("device_id")["downtime"].sum().reset_index()
298
+ if len(downtime_data) > 5:
299
+ downtime_data = downtime_data.nlargest(5, "downtime")
300
+ fig = px.bar(
301
+ downtime_data,
302
+ x="device_id",
303
+ y="downtime",
304
+ title="Downtime per Device",
305
+ labels={"device_id": "Device ID", "downtime": "Downtime (Hours)"}
306
+ )
307
+ fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
308
+ return fig
309
  except Exception as e:
310
+ logging.error(f"Failed to create downtime chart: {str(e)}")
311
+ return create_placeholder_chart("Downtime per Device")
312
+
313
+ # Create daily log trends chart
314
+ def create_daily_log_trends_chart(df):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  try:
316
+ if df.empty or "timestamp" not in df.columns:
317
+ logging.warning("Insufficient data for daily log trends chart")
318
+ return create_placeholder_chart("Daily Log Trends")
319
+ df['date'] = pd.to_datetime(df['timestamp'], errors='coerce').dt.date
320
+ daily_logs = df.groupby('date').size().reset_index(name='log_count')
321
+ if daily_logs.empty:
322
+ return create_placeholder_chart("Daily Log Trends")
323
+ fig = px.line(
324
+ daily_logs,
325
+ x='date',
326
+ y='log_count',
327
+ title="Daily Log Trends",
328
+ labels={"date": "Date", "log_count": "Number of Logs"}
329
+ )
330
+ fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
331
+ return fig
 
 
 
 
 
 
 
332
  except Exception as e:
333
+ logging.error(f"Failed to create daily log trends chart: {str(e)}")
334
+ return create_placeholder_chart("Daily Log Trends")
335
 
336
+ # Create weekly uptime chart
337
+ def create_weekly_uptime_chart(df):
338
  try:
339
+ if df.empty or "timestamp" not in df.columns or "usage_hours" not in df.columns or "downtime" not in df.columns:
340
+ logging.warning("Insufficient data for weekly uptime chart")
341
+ return create_placeholder_chart("Weekly Uptime Percentage")
342
+ df['week'] = pd.to_datetime(df['timestamp'], errors='coerce').dt.isocalendar().week
343
+ df['year'] = pd.to_datetime(df['timestamp'], errors='coerce').dt.year
344
+ weekly_data = df.groupby(['year', 'week']).agg({
345
+ 'usage_hours': 'sum',
346
+ 'downtime': 'sum'
347
+ }).reset_index()
348
+ weekly_data['uptime_percent'] = (weekly_data['usage_hours'] / (weekly_data['usage_hours'] + weekly_data['downtime'])) * 100
349
+ weekly_data['year_week'] = weekly_data['year'].astype(str) + '-W' + weekly_data['week'].astype(str)
350
+ if weekly_data.empty:
351
+ return create_placeholder_chart("Weekly Uptime Percentage")
352
+ fig = px.bar(
353
+ weekly_data,
354
+ x='year_week',
355
+ y='uptime_percent',
356
+ title="Weekly Uptime Percentage",
357
+ labels={"year_week": "Year-Week", "uptime_percent": "Uptime %"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  )
359
+ fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
360
+ return fig
361
+ except Exception as e:
362
+ logging.error(f"Failed to create weekly uptime chart: {str(e)}")
363
+ return create_placeholder_chart("Weekly Uptime Percentage")
364
+
365
+ # Create anomaly alerts chart
366
+ def create_anomaly_alerts_chart(anomalies_df):
367
+ try:
368
+ if anomalies_df is None or anomalies_df.empty or "timestamp" not in anomalies_df.columns:
369
+ logging.warning("Insufficient data for anomaly alerts chart")
370
+ return create_placeholder_chart("Anomaly Alerts Over Time")
371
+ anomalies_df['date'] = pd.to_datetime(anomalies_df['timestamp'], errors='coerce').dt.date
372
+ anomaly_counts = anomalies_df.groupby('date').size().reset_index(name='anomaly_count')
373
+ if anomaly_counts.empty:
374
+ return create_placeholder_chart("Anomaly Alerts Over Time")
375
+ fig = px.scatter(
376
+ anomaly_counts,
377
+ x='date',
378
+ y='anomaly_count',
379
+ title="Anomaly Alerts Over Time",
380
+ labels={"date": "Date", "anomaly_count": "Number of Anomalies"}
381
  )
382
+ fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
383
+ return fig
384
+ except Exception as e:
385
+ logging.error(f"Failed to create anomaly alerts chart: {str(e)}")
386
+ return create_placeholder_chart("Anomaly Alerts Over Time")
387
+
388
+ # Generate device cards
389
+ def generate_device_cards(df):
390
+ try:
391
+ if df.empty:
392
+ return '<p>No devices available to display.</p>'
393
+ device_stats = df.groupby('device_id').agg({
394
+ 'status': 'last',
395
+ 'timestamp': 'max',
396
+ }).reset_index()
397
+ device_stats['count'] = df.groupby('device_id').size().reindex(device_stats['device_id']).values
398
+ device_stats['health'] = device_stats['status'].map({
399
+ 'Active': 'Healthy',
400
+ 'Inactive': 'Unhealthy',
401
+ 'Pending': 'Warning'
402
+ }).fillna('Unknown')
403
+ cards_html = '<div style="display: flex; flex-wrap: wrap; gap: 20px;">'
404
+ for _, row in device_stats.iterrows():
405
+ health_color = {'Healthy': 'green', 'Unhealthy': 'red', 'Warning': 'orange', 'Unknown': 'gray'}.get(row['health'], 'gray')
406
+ timestamp_str = str(row['timestamp']) if pd.notna(row['timestamp']) else 'Unknown'
407
+ cards_html += f"""
408
+ <div style="border: 1px solid #e0e0e0; padding: 10px; border-radius: 5px; width: 200px;">
409
+ <h4>Device: {row['device_id']}</h4>
410
+ <p><b>Health:</b> <span style="color: {health_color}">{row['health']}</span></p>
411
+ <p><b>Usage Count:</b> {row['count']}</p>
412
+ <p><b>Last Log:</b> {timestamp_str}</p>
413
+ </div>
414
+ """
415
+ cards_html += '</div>'
416
+ return cards_html
417
+ except Exception as e:
418
+ logging.error(f"Failed to generate device cards: {str(e)}")
419
+ return f'<p>Error generating device cards: {str(e)}</p>'
420
+
421
+ # Generate PDF content
422
+ def generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights, device_cards_html, daily_log_chart, weekly_uptime_chart, anomaly_alerts_chart, downtime_chart):
423
+ if not reportlab_available:
424
  return None
425
+ try:
426
+ pdf_path = f"status_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
427
+ doc = SimpleDocTemplate(pdf_path, pagesize=letter)
428
+ styles = getSampleStyleSheet()
429
+ story = []
430
+
431
+ def safe_paragraph(text, style):
432
+ return Paragraph(str(text).replace('\n', '<br/>'), style) if text else Paragraph("", style)
433
+
434
+ story.append(Paragraph("LabOps Status Report", styles['Title']))
435
+ story.append(Paragraph(f"Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", styles['Normal']))
436
+ story.append(Spacer(1, 12))
437
+
438
+ story.append(Paragraph("Summary Report", styles['Heading2']))
439
+ story.append(safe_paragraph(summary, styles['Normal']))
440
+ story.append(Spacer(1, 12))
441
+
442
+ story.append(Paragraph("Log Preview", styles['Heading2']))
443
+ if not preview_df.empty:
444
+ data = [preview_df.columns.tolist()] + preview_df.head(5).values.tolist()
445
+ table = Table(data)
446
+ table.setStyle(TableStyle([
447
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
448
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
449
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
450
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
451
+ ('FONTSIZE', (0, 0), (-1, 0), 12),
452
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
453
+ ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
454
+ ('TEXTCOLOR', (0, 1), (-1, -1), colors.black),
455
+ ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
456
+ ('FONTSIZE', (0, 1), (-1, -1), 10),
457
+ ('GRID', (0, 0), (-1, -1), 1, colors.black)
458
+ ]))
459
+ story.append(table)
460
+ else:
461
+ story.append(safe_paragraph("No preview available.", styles['Normal']))
462
+ story.append(Spacer(1, 12))
463
+
464
+ story.append(Paragraph("Device Cards", styles['Heading2']))
465
+ device_cards_text = device_cards_html.replace('<div>', '').replace('</div>', '\n').replace('<h4>', '').replace('</h4>', '\n').replace('<p>', '').replace('</p>', '\n').replace('<b>', '').replace('</b>', '').replace('<span style="color: green">', '').replace('<span style="color: red">', '').replace('<span style="color: orange">', '').replace('<span style="color: gray">', '').replace('</span>', '')
466
+ story.append(safe_paragraph(device_cards_text, styles['Normal']))
467
+ story.append(Spacer(1, 12))
468
+
469
+ story.append(Paragraph("Anomaly Detection", styles['Heading2']))
470
+ story.append(safe_paragraph(anomalies, styles['Normal']))
471
+ story.append(Spacer(1, 12))
472
+
473
+ story.append(Paragraph("AMC Reminders", styles['Heading2']))
474
+ story.append(safe_paragraph(amc_reminders, styles['Normal']))
475
+ story.append(Spacer(1, 12))
476
+
477
+ story.append(Paragraph("Dashboard Insights", styles['Heading2']))
478
+ story.append(safe_paragraph(insights, styles['Normal']))
479
+ story.append(Spacer(1, 12))
480
+
481
+ story.append(Paragraph("Charts", styles['Heading2']))
482
+ story.append(Paragraph("[Chart placeholders - see dashboard for visuals]", styles['Normal']))
483
+
484
+ doc.build(story)
485
+ logging.info(f"PDF generated at {pdf_path}")
486
+ return pdf_path
487
  except Exception as e:
488
+ logging.error(f"Failed to generate PDF: {str(e)}")
489
  return None
490
 
491
+ # Main processing function
492
+ async def process_logs(file_obj, lab_site_filter, equipment_type_filter, date_range, cached_df_state, last_modified_state):
493
+ start_time = time.time()
 
494
  try:
495
+ if not file_obj:
496
+ return "No file uploaded.", "<p>No data available.</p>", None, '<p>No device cards available.</p>', None, None, None, None, "No anomalies detected.", "No AMC reminders.", "No insights generated.", None, cached_df_state, last_modified_state
497
+
498
+ file_path = file_obj.name
499
+ current_modified_time = os.path.getmtime(file_path)
500
+
501
+ # Read file only if it's new or modified
502
+ if cached_df_state is None or current_modified_time != last_modified_state:
503
+ logging.info(f"Processing new or modified file: {file_path}")
504
+ if not file_path.endswith(".csv"):
505
+ return "Please upload a CSV file.", "<p>Invalid file format.</p>", None, '<p>No device cards available.</p>', None, None, None, None, "", "", "", None, cached_df_state, last_modified_state
506
+
507
+ required_columns = ["device_id", "log_type", "status", "timestamp", "usage_hours", "downtime", "amc_date"]
508
+ dtypes = {
509
+ "device_id": "string",
510
+ "log_type": "string",
511
+ "status": "string",
512
+ "usage_hours": "float32",
513
+ "downtime": "float32",
514
+ "amc_date": "string"
515
+ }
516
+ df = pd.read_csv(file_path, dtype=dtypes)
517
+ missing_columns = [col for col in required_columns if col not in df.columns]
518
+ if missing_columns:
519
+ return f"Missing columns: {missing_columns}", "<p>Missing required columns.</p>", None, '<p>No device cards available.</p>', None, None, None, None, "", "", "", None, cached_df_state, last_modified_state
520
+
521
+ df["timestamp"] = pd.to_datetime(df["timestamp"], errors='coerce')
522
+ df["amc_date"] = pd.to_datetime(df["amc_date"], errors='coerce')
523
+ if df["timestamp"].dt.tz is None:
524
+ df["timestamp"] = df["timestamp"].dt.tz_localize('UTC').dt.tz_convert('Asia/Kolkata')
525
+ if df.empty:
526
+ return "No data available.", "<p>No data available.</p>", None, '<p>No device cards available.</p>', None, None, None, None, "", "", "", None, df, current_modified_time
527
+ else:
528
+ df = cached_df_state
529
+
530
+ # Apply filters
531
+ filtered_df = df.copy()
532
+ if lab_site_filter and lab_site_filter != 'All' and 'lab_site' in filtered_df.columns:
533
+ filtered_df = filtered_df[filtered_df['lab_site'] == lab_site_filter]
534
+ if equipment_type_filter and equipment_type_filter != 'All' and 'equipment_type' in filtered_df.columns:
535
+ filtered_df = filtered_df[filtered_df['equipment_type'] == equipment_type_filter]
536
+ if date_range is not None:
537
+ if isinstance(date_range, (int, float)):
538
+ # Convert single value to a range for a single day
539
+ days = int(date_range)
540
+ date_range = [days, days]
541
+ logging.info(f"Converted single value {days} to range {date_range}")
542
+ if len(date_range) != 2 or not all(isinstance(x, (int, float)) for x in date_range):
543
+ logging.error(f"Invalid date range format: {date_range}. Expected [start, end] or single integer.")
544
+ return "Invalid date range format. Please use [start, end] (e.g., [-7, 0]) or a single integer (e.g., -1).", "<p>Error processing data.</p>", None, '<p>Error processing data.</p>', None, None, None, None, "", "", "", None, df, current_modified_time
545
+ days_start, days_end = date_range
546
+ today = pd.to_datetime(datetime.now()).tz_localize('Asia/Kolkata')
547
+ start_date = today + pd.Timedelta(days=days_start)
548
+ end_date = today + pd.Timedelta(days=days_end) + pd.Timedelta(days=1) - pd.Timedelta(seconds=1)
549
+ start_date = start_date.tz_convert('Asia/Kolkata') if start_date.tzinfo else start_date.tz_localize('Asia/Kolkata')
550
+ end_date = end_date.tz_convert('Asia/Kolkata') if end_date.tzinfo else end_date.tz_localize('Asia/Kolkata')
551
+ logging.info(f"Date range filter applied: start_date={start_date}, end_date={end_date}")
552
+ logging.info(f"Before date filter: {len(filtered_df)} rows")
553
+ filtered_df = filtered_df[(filtered_df['timestamp'] >= start_date) & (filtered_df['timestamp'] <= end_date)]
554
+ logging.info(f"After date filter: {len(filtered_df)} rows")
555
+ if days_start > days_end:
556
+ logging.warning("Start date is after end date; results may be empty or unexpected.")
557
+
558
+ if filtered_df.empty:
559
+ return "No data after applying filters.", "<p>No data after filters.</p>", None, '<p>No device cards available.</p>', None, None, None, None, "", "", "", None, df, current_modified_time
560
+
561
+ # Generate table for preview
562
+ preview_df = filtered_df[['device_id', 'log_type', 'status', 'timestamp', 'usage_hours', 'downtime', 'amc_date']].head(5)
563
+ preview_html = preview_df.to_html(index=False, classes='table table-striped', border=0)
564
+
565
+ # Run critical tasks concurrently
566
+ with ThreadPoolExecutor(max_workers=2) as executor:
567
+ future_anomalies = executor.submit(detect_anomalies, filtered_df)
568
+ future_amc = executor.submit(check_amc_reminders, filtered_df, datetime.now())
569
+
570
+ summary = f"Step 1: Summary Report\n{summarize_logs(filtered_df)}"
571
+ anomalies, anomalies_df = future_anomalies.result()
572
+ anomalies = f"Anomaly Detection\n{anomalies}"
573
+ amc_reminders, reminders_df = future_amc.result()
574
+ amc_reminders = f"AMC Reminders\n{amc_reminders}"
575
+ insights = f"Dashboard Insights\n{generate_dashboard_insights(filtered_df)}"
576
+
577
+ # Generate charts sequentially
578
+ usage_chart = create_usage_chart(filtered_df)
579
+ downtime_chart = create_downtime_chart(filtered_df)
580
+ daily_log_chart = create_daily_log_trends_chart(filtered_df)
581
+ weekly_uptime_chart = create_weekly_uptime_chart(filtered_df)
582
+ anomaly_alerts_chart = create_anomaly_alerts_chart(anomalies_df)
583
+ device_cards = generate_device_cards(filtered_df)
584
+
585
+ # Save to Salesforce after all other processing
586
+ save_to_salesforce(filtered_df, reminders_df)
587
+ create_salesforce_reports(filtered_df)
588
+
589
+ elapsed_time = time.time() - start_time
590
+ logging.info(f"Processing completed in {elapsed_time:.2f} seconds")
591
+ if elapsed_time > 3:
592
+ logging.warning(f"Processing time exceeded 3 seconds: {elapsed_time:.2f} seconds")
593
+
594
+ return (summary, preview_html, usage_chart, device_cards, daily_log_chart, weekly_uptime_chart, anomaly_alerts_chart, downtime_chart, anomalies, amc_reminders, insights, None, df, current_modified_time)
595
  except Exception as e:
596
+ logging.error(f"Failed to process file: {str(e)}")
597
+ return f"Error: {str(e)}", "<p>Error processing data.</p>", None, '<p>Error processing data.</p>', None, None, None, None, "", "", "", None, cached_df_state, last_modified_state
598
 
599
+ # Generate PDF separately
600
+ async def generate_pdf(summary, preview_html, usage_chart, device_cards, daily_log_chart, weekly_uptime_chart, anomaly_alerts_chart, downtime_chart, anomalies, amc_reminders, insights):
601
+ try:
602
+ preview_df = pd.read_html(preview_html)[0]
603
+ pdf_file = generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights, device_cards, daily_log_chart, weekly_uptime_chart, anomaly_alerts_chart, downtime_chart)
604
+ return pdf_file
605
+ except Exception as e:
606
+ logging.error(f"Failed to generate PDF: {str(e)}")
607
+ return None
608
 
609
+ # Update filters
610
+ def update_filters(file_obj, current_file_state):
611
+ if not file_obj or file_obj.name == current_file_state:
612
+ return gr.update(), gr.update(), current_file_state
613
  try:
614
+ with open(file_obj.name, 'rb') as f:
615
+ csv_content = f.read().decode('utf-8')
616
+ df = pd.read_csv(io.StringIO(csv_content))
617
+ df['timestamp'] = pd.to_datetime(df['timestamp'], errors='coerce')
618
+
619
+ lab_site_options = ['All'] + [site for site in df['lab_site'].dropna().astype(str).unique().tolist() if site.strip()] if 'lab_site' in df.columns else ['All']
620
+ equipment_type_options = ['All'] + [equip for equip in df['equipment_type'].dropna().astype(str).unique().tolist() if equip.strip()] if 'equipment_type' in df.columns else ['All']
621
+
622
+ return gr.update(choices=lab_site_options, value='All'), gr.update(choices=equipment_type_options, value='All'), file_obj.name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  except Exception as e:
624
+ logging.error(f"Failed to update filters: {str(e)}")
625
+ return gr.update(choices=['All'], value='All'), gr.update(choices=['All'], value='All'), current_file_state
626
 
627
+ # Gradio Interface
628
+ try:
629
+ logging.info("Initializing Gradio interface...")
630
+ with gr.Blocks(css="""
631
+ .dashboard-container {border: 1px solid #e0e0e0; padding: 10px; border-radius: 5px;}
632
+ .dashboard-title {font-size: 24px; font-weight: bold; margin-bottom: 5px;}
633
+ .dashboard-section {margin-bottom: 20px;}
634
+ .dashboard-section h3 {font-size: 18px; margin-bottom: 2px;}
635
+ .dashboard-section p {margin: 1px 0; line-height: 1.2;}
636
+ .dashboard-section ul {margin: 2px 0; padding-left: 20px;}
637
+ .table {width: 100%; border-collapse: collapse;}
638
+ .table th, .table td {border: 1px solid #ddd; padding: 8px; text-align: left;}
639
+ .table th {background-color: #f2f2f2;}
640
+ .table tr:nth-child(even) {background-color: #f9f9f9;}
641
+ """) as iface:
642
+ gr.Markdown("<h1>LabOps Log Analyzer Dashboard</h1>")
643
+ gr.Markdown("Upload a CSV file to analyze. Click 'Analyze' to refresh the dashboard. Use 'Export PDF' for report download. Date Range can be [start, end] (e.g., [-7, 0] for June 11 to June 18) or a single integer (e.g., -1 for June 17).")
644
+
645
+ last_modified_state = gr.State(value=None)
646
+ current_file_state = gr.State(value=None)
647
+ cached_df_state = gr.State(value=None)
648
 
649
  with gr.Row():
650
+ with gr.Column(scale=1):
651
+ file_input = gr.File(label="Upload Logs (CSV)", file_types=[".csv"])
652
+ with gr.Group():
653
+ gr.Markdown("### Filters")
654
+ lab_site_filter = gr.Dropdown(label="Lab Site", choices=['All'], value='All', interactive=True)
655
+ equipment_type_filter = gr.Dropdown(label="Equipment Type", choices=['All'], value='All', interactive=True)
656
+ date_range_filter = gr.Slider(label="Date Range (Days from Today)", minimum=-365, maximum=0, step=1, value=[-7, 0], interactive=True)
657
+ submit_button = gr.Button("Analyze", variant="primary")
658
+ pdf_button = gr.Button("Export PDF", variant="secondary")
659
+
660
+ with gr.Column(scale=2):
661
+ with gr.Group(elem_classes="dashboard-container"):
662
+ gr.Markdown("<div class='dashboard-title'>Analysis Results</div>")
663
+ with gr.Group(elem_classes="dashboard-section"):
664
+ gr.Markdown("### Step 1: Summary Report")
665
+ summary_output = gr.Markdown()
666
+ with gr.Group(elem_classes="dashboard-section"):
667
+ gr.Markdown("### Step 2: Log Preview")
668
+ preview_output = gr.HTML()
669
+ with gr.Group(elem_classes="dashboard-section"):
670
+ gr.Markdown("### Device Cards")
671
+ device_cards_output = gr.HTML()
672
+ with gr.Group(elem_classes="dashboard-section"):
673
+ gr.Markdown("### Charts")
674
+ with gr.Tab("Usage Hours per Device"):
675
+ usage_chart_output = gr.Plot()
676
+ with gr.Tab("Downtime per Device"):
677
+ downtime_chart_output = gr.Plot()
678
+ with gr.Tab("Daily Log Trends"):
679
+ daily_log_trends_output = gr.Plot()
680
+ with gr.Tab("Weekly Uptime Percentage"):
681
+ weekly_uptime_output = gr.Plot()
682
+ with gr.Tab("Anomaly Alerts"):
683
+ anomaly_alerts_output = gr.Plot()
684
+ with gr.Group(elem_classes="dashboard-section"):
685
+ gr.Markdown("### Step 4: Anomaly Detection")
686
+ anomaly_output = gr.Markdown()
687
+ with gr.Group(elem_classes="dashboard-section"):
688
+ gr.Markdown("### Step 5: AMC Reminders")
689
+ amc_output = gr.Markdown()
690
+ with gr.Group(elem_classes="dashboard-section"):
691
+ gr.Markdown("### Step 6: Insights")
692
+ insights_output = gr.Markdown()
693
+ with gr.Group(elem_classes="dashboard-section"):
694
+ gr.Markdown("### Export Report")
695
+ pdf_output = gr.File(label="Download Status Report as PDF")
696
+
697
+ file_input.change(
698
+ fn=update_filters,
699
+ inputs=[file_input, current_file_state],
700
+ outputs=[lab_site_filter, equipment_type_filter, current_file_state],
701
+ queue=False
702
  )
703
+
704
+ submit_button.click(
705
+ fn=process_logs,
706
+ inputs=[file_input, lab_site_filter, equipment_type_filter, date_range_filter, cached_df_state, last_modified_state],
707
+ outputs=[summary_output, preview_output, usage_chart_output, device_cards_output, daily_log_trends_output, weekly_uptime_output, anomaly_alerts_output, downtime_chart_output, anomaly_output, amc_output, insights_output, pdf_output, cached_df_state, last_modified_state]
708
  )
709
+
710
+ pdf_button.click(
711
+ fn=generate_pdf,
712
+ inputs=[summary_output, preview_output, usage_chart_output, device_cards_output, daily_log_trends_output, weekly_uptime_output, anomaly_alerts_output, downtime_chart_output, anomaly_output, amc_output, insights_output],
713
+ outputs=[pdf_output]
714
  )
715
+
716
+ logging.info("Gradio interface initialized successfully")
717
+ except Exception as e:
718
+ logging.error(f"Failed to initialize Gradio interface: {str(e)}")
719
+ raise e
720
 
721
  if __name__ == "__main__":
722
+ try:
723
+ logging.info("Launching Gradio interface...")
724
+ iface.launch(server_name="0.0.0.0", server_port=7860, debug=True, share=False)
725
+ logging.info("Gradio interface launched successfully")
726
+ except Exception as e:
727
+ logging.error(f"Failed to launch Gradio interface: {str(e)}")
728
+ print(f"Error launching app: {str(e)}")
729
+ raise e