RathodHarish commited on
Commit
4385ddd
·
verified ·
1 Parent(s): d2060b3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +111 -445
app.py CHANGED
@@ -1,447 +1,113 @@
1
  import gradio as gr
2
  import pandas as pd
3
- import plotly.express as px
4
- from sklearn.ensemble import IsolationForest
5
- from reportlab.lib.pagesizes import letter
6
- from reportlab.pdfgen import canvas
7
- from transformers import pipeline
8
- import datetime
9
- import os
10
- import logging
11
- import re
12
- import numpy as np
13
- import torch
14
- import tempfile
15
-
16
- # Configure logging to track application events and errors
17
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
18
- logger = logging.getLogger(__name__)
19
-
20
- # Log versions of key dependencies for debugging
21
- logger.info(f"NumPy version: {np.__version__}")
22
- logger.info(f"Pandas version: {pd.__version__}")
23
- logger.info(f"PyTorch version: {torch.__version__}")
24
- logger.info(f"Gradio version: {gr.__version__}")
25
-
26
- # Initialize list to store log messages for UI display
27
- log_messages = []
28
-
29
- # Custom logging handler to capture logs for Gradio UI
30
- class UILogHandler(logging.Handler):
31
- def emit(self, record):
32
- log_entry = self.format(record)
33
- log_messages.append(log_entry)
34
-
35
- ui_handler = UILogHandler()
36
- ui_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
37
- logger.addHandler(ui_handler)
38
-
39
- # Initialize Hugging Face summarization model (BART) for generating summaries
40
- try:
41
- summarizer = pipeline(
42
- "summarization",
43
- model="facebook/bart-large-cnn",
44
- device=0 if torch.cuda.is_available() else -1, # Use GPU if available
45
- framework="pt"
46
- )
47
- logger.info("Hugging Face summarization pipeline initialized")
48
- except Exception as e:
49
- logger.error(f"Failed to initialize Hugging Face pipeline: {e}")
50
- summarizer = None
51
-
52
- # Global DataFrame to store logs data
53
- logs = pd.DataFrame()
54
-
55
- # Load and validate logs from uploaded CSV file
56
- def load_logs(logs_file):
57
- global logs
58
- try:
59
- if logs_file is None:
60
- logger.error("No logs CSV file uploaded")
61
- return pd.DataFrame()
62
- # Write uploaded file to a temporary CSV
63
- with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
64
- tmp.write(logs_file.read())
65
- tmp_path = tmp.name
66
- logs = pd.read_csv(tmp_path)
67
- os.unlink(tmp_path) # Remove temporary file
68
- # Define required columns; 'comments' is optional
69
- required_columns = ['device_id', 'lab_id', 'timestamp', 'status', 'usage_count', 'type']
70
- if not all(col in logs.columns for col in required_columns):
71
- logger.error(f"Missing required columns in logs: {required_columns}")
72
- return pd.DataFrame()
73
- # Add 'comments' column if missing, filling with 'No comment'
74
- if 'comments' not in logs.columns:
75
- logs['comments'] = 'No comment'
76
- logger.info("Added missing 'comments' column with default value 'No comment'")
77
- # Convert timestamp to datetime and check for invalid entries
78
- logs['timestamp'] = pd.to_datetime(logs['timestamp'], errors='coerce')
79
- if logs['timestamp'].isna().any():
80
- logger.error("Invalid timestamps detected in logs")
81
- return pd.DataFrame()
82
- logger.info("Logs loaded successfully from uploaded CSV")
83
- return logs
84
- except Exception as e:
85
- logger.error(f"Error loading logs: {e}")
86
- return pd.DataFrame()
87
-
88
- # Update dropdown menus based on uploaded CSV data
89
- def update_dropdowns(logs_file):
90
- global logs
91
- logs = load_logs(logs_file)
92
- if logs.empty:
93
- return (
94
- gr.Dropdown(choices=[], value=None, label="Select Lab"),
95
- gr.Dropdown(choices=[], value=None, label="Select Equipment Type"),
96
- "<p style='color: red;'>Please upload a valid logs CSV file to proceed.</p>",
97
- False # Disable Generate Dashboard button
98
- )
99
- # Extract unique labs and equipment types, sorted for better UX
100
- lab_choices = sorted(logs['lab_id'].unique().tolist())
101
- equipment_choices = sorted(logs['type'].unique().tolist())
102
- upload_status = "<p style='color: green;'>Logs loaded successfully.</p>"
103
- return (
104
- gr.Dropdown(choices=lab_choices, value=lab_choices[0] if lab_choices else None, label="Select Lab"),
105
- gr.Dropdown(choices=equipment_choices, value=equipment_choices[0] if equipment_choices else None, label="Select Equipment Type"),
106
- upload_status,
107
- True # Enable Generate Dashboard button
108
- )
109
-
110
- # Detect anomalies using Isolation Forest
111
- def detect_anomalies(logs):
112
- try:
113
- if logs.empty:
114
- logger.warning("No logs available for anomaly detection")
115
- return logs
116
- model = IsolationForest(contamination=0.1, random_state=42)
117
- usage_data = logs[['usage_count']].copy()
118
- logs['anomaly'] = model.fit_predict(usage_data)
119
- logger.info("Anomaly detection completed")
120
- return logs
121
- except Exception as e:
122
- logger.error(f"Error in anomaly detection: {e}")
123
- return logs
124
-
125
- # Generate text summary including comments
126
- def generate_text_summary(logs):
127
- if summarizer is None or logs.empty:
128
- return "No summary available due to missing data or model initialization."
129
- try:
130
- log_text = "\n".join(
131
- f"Device {row['device_id']} in lab {row['lab_id']} on {row['timestamp'].strftime('%Y-%m-%d %H:%M:%S')} "
132
- f"was {row['status']} with usage count {row['usage_count']} (Type: {row['type']}). "
133
- f"Comment: {row['comments']}"
134
- for _, row in logs.iterrows()
135
- )
136
- summary = summarizer(log_text, max_length=150, min_length=40, do_sample=False)[0]['summary_text']
137
- logger.info("Text summary generated successfully")
138
- return summary
139
- except Exception as e:
140
- logger.error(f"Error generating text summary: {e}")
141
- return f"Error generating summary: {str(e)}"
142
-
143
- # Generate executive insights including comments
144
- def generate_executive_insights(logs, anomalies):
145
- if summarizer is None or logs.empty:
146
- return "No executive insights available due to missing data or model initialization."
147
- try:
148
- log_text = "\n".join(
149
- f"Device {row['device_id']} in lab {row['lab_id']} on {row['timestamp'].strftime('%Y-%m-%d %H:%M:%S')} "
150
- f"was {row['status']} with usage count {row['usage_count']} (Type: {row['type']}). "
151
- f"Comment: {row['comments']}"
152
- for _, row in logs.iterrows()
153
- )
154
- anomaly_text = "\n".join(
155
- f"Device {row['device_id']} showed anomalous usage count {row['usage_count']} on "
156
- f"{row['timestamp'].strftime('%Y-%m-%d %H:%M:%S')}. Comment: {row['comments']}"
157
- for _, row in anomalies.iterrows()
158
- ) if not anomalies.empty else "No anomalies detected."
159
- prompt = (
160
- f"Summarize the following lab operations data into concise executive insights:\n\n"
161
- f"Logs:\n{log_text}\n\n"
162
- f"Anomalies:\n{anomaly_text}\n\n"
163
- f"Provide high-level insights for lab managers, focusing on operational status, issues, and comments."
164
- )
165
- insights = summarizer(prompt, max_length=200, min_length=50, do_sample=False)[0]['summary_text']
166
- logger.info("Executive insights generated successfully")
167
- return insights
168
- except Exception as e:
169
- logger.error(f"Error generating executive insights: {e}")
170
- return f"Error generating insights: {str(e)}"
171
-
172
- # Generate PDF report with summary, insights, and comments
173
- def generate_pdf(lab, equipment_type, filtered_logs, summary, insights):
174
- try:
175
- pdf_file = f"labops_report_{lab}_{equipment_type}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
176
- c = canvas.Canvas(pdf_file, pagesize=letter)
177
- c.setFont("Helvetica-Bold", 16)
178
- c.drawString(100, 750, "LabOps Dashboard Report")
179
- c.setFont("Helvetica", 12)
180
- c.drawString(100, 730, f"Lab: {lab}, Equipment Type: {equipment_type}")
181
-
182
- # Add summary
183
- c.setFont("Helvetica-Bold", 14)
184
- c.drawString(100, 700, "Summary")
185
- c.setFont("Helvetica", 12)
186
- y = 680
187
- for line in summary.split('\n'):
188
- c.drawString(100, y, line[:80])
189
- y -= 20
190
- if y < 100:
191
- c.showPage()
192
- y = 750
193
-
194
- # Add executive insights
195
- c.setFont("Helvetica-Bold", 14)
196
- c.drawString(100, y, "Executive Insights")
197
- c.setFont("Helvetica", 12)
198
- y -= 20
199
- for line in insights.split('\n'):
200
- c.drawString(100, y, line[:80])
201
- y -= 20
202
- if y < 100:
203
- c.showPage()
204
- y = 750
205
-
206
- # Add device status with comments
207
- c.setFont("Helvetica-Bold", 14)
208
- c.drawString(100, y, "Device Status Summary")
209
- c.setFont("Helvetica", 12)
210
- y -= 20
211
- for _, row in filtered_logs.iterrows():
212
- c.drawString(100, y, f"Device: {row['device_id']}, Status: {row['status']}, "
213
- f"Usage: {row['usage_count']}, Comment: {row['comments'][:30]}")
214
- y -= 20
215
- if y < 100:
216
- c.showPage()
217
- y = 750
218
-
219
- c.save()
220
- if not os.path.exists(pdf_file):
221
- logger.error("PDF file was not created.")
222
- return None
223
- logger.info(f"PDF report generated: {pdf_file}")
224
- return pdf_file
225
- except Exception as e:
226
- logger.error(f"Error generating PDF: {e}")
227
- return None
228
-
229
- # Validate date format (YYYY-MM-DD)
230
- def validate_date(date_str):
231
- pattern = r'^\d{4}-\d{2}-\d{2}$'
232
- if not isinstance(date_str, str) or not re.match(pattern, date_str):
233
- return False
234
- try:
235
- pd.to_datetime(date_str, format='%Y-%m-%d')
236
- return True
237
- except ValueError:
238
- return False
239
-
240
- # Render the dashboard with filtered data
241
- def render_dashboard(lab, equipment_type, start_date, end_date):
242
- try:
243
- logger.info(f"Rendering dashboard with filters: lab={lab}, equipment_type={equipment_type}, "
244
- f"start_date={start_date}, end_date={end_date}")
245
- # Validate inputs
246
- if not lab or not equipment_type:
247
- return (
248
- "<p style='color: red;'>Please select both Lab and Equipment Type.</p>",
249
- None, None, "<p style='color: red;'>Invalid input</p>",
250
- None, "No summary available.", "No insights available.",
251
- "\n".join(log_messages[-10:]) or "No logs available."
252
- )
253
- # Validate and adjust dates
254
- if not validate_date(start_date):
255
- logger.warning(f"Invalid start_date format: {start_date}. Using default 2025-05-01.")
256
- start_date = "2025-05-01"
257
- if not validate_date(end_date):
258
- logger.warning(f"Invalid end_date format: {end_date}. Using default 2025-05-30.")
259
- end_date = "2025-05-30"
260
- start_dt = pd.to_datetime(start_date, format='%Y-%m-%d')
261
- end_dt = pd.to_datetime(end_date, format='%Y-%m-%d')
262
- if start_dt > end_dt:
263
- logger.warning("start_date is after end_date. Swapping dates.")
264
- start_dt, end_dt = end_dt, start_dt
265
- # Filter logs by lab, equipment type, and date range
266
- filtered_logs = logs[(logs['lab_id'] == lab) & (logs['type'] == equipment_type)]
267
- filtered_logs = filtered_logs[
268
- (filtered_logs['timestamp'] >= start_dt) &
269
- (filtered_logs['timestamp'] <= end_dt)
270
- ]
271
- if filtered_logs.empty:
272
- logger.warning("No data available for the selected filters")
273
- return (
274
- "<p style='color: orange;'>No devices found for the selected filters.</p>",
275
- None, None, "<p>No anomalies detected.</p>",
276
- None, "No summary available.", "No insights available.",
277
- "\n".join(log_messages[-10:]) or "No logs available."
278
- )
279
- # Apply anomaly detection
280
- filtered_logs = detect_anomalies(filtered_logs)
281
- # Generate device cards for display
282
- device_cards = "<div style='display: flex; flex-wrap: wrap; gap: 20px;'>"
283
- for _, row in filtered_logs.iterrows():
284
- status_color = "green" if row['status'] == "active" else "red"
285
- device_cards += f"""
286
- <div style='border: 1px solid #ccc; padding: 15px; margin: 10px; border-radius: 8px; width: 250px; background-color: #f9f9f9;'>
287
- <h3 style='margin: 0; font-size: 18px;'>Device: {row['device_id']}</h3>
288
- <p style='color: {status_color};'>Status: {row['status'].capitalize()}</p>
289
- <p>Usage Count: {row['usage_count']}</p>
290
- <p>Comment: {row['comments'][:30]}</p>
291
- <p>Last Log: {row['timestamp'].strftime('%Y-%m-%d %H:%M:%S')}</p>
292
- </div>
293
- """
294
- device_cards += "</div>"
295
- # Generate usage trend chart
296
- fig_usage = px.line(
297
- filtered_logs,
298
- x='timestamp',
299
- y='usage_count',
300
- color='device_id',
301
- title="Daily Usage Trends",
302
- template="plotly_white"
303
- ).update_layout(
304
- xaxis_title="Timestamp",
305
- yaxis_title="Usage Count",
306
- margin=dict(l=20, r=20, t=40, b=20)
307
- )
308
- # Generate uptime chart
309
- uptime = filtered_logs.groupby('device_id')['status'].apply(lambda x: (x == 'active').mean() * 100)
310
- fig_uptime = px.bar(
311
- uptime,
312
- x=uptime.index,
313
- y=uptime,
314
- title="Weekly Uptime %",
315
- template="plotly_white"
316
- ).update_layout(
317
- xaxis_title="Device ID",
318
- yaxis_title="Uptime %",
319
- margin=dict(l=20, r=20, t=40, b=20)
320
- )
321
- # Generate anomaly table
322
- anomalies = filtered_logs[filtered_logs['anomaly'] == -1] if 'anomaly' in filtered_logs.columns else pd.DataFrame()
323
- anomaly_table = anomalies[['device_id', 'timestamp', 'usage_count', 'comments']].to_html(
324
- index=False,
325
- classes="table table-striped",
326
- border=0
327
- ) if not anomalies.empty else "<p>No anomalies detected.</p>"
328
- # Generate summary and insights
329
- summary = generate_text_summary(filtered_logs)
330
- insights = generate_executive_insights(filtered_logs, anomalies)
331
- # Generate PDF report
332
- pdf_data = generate_pdf(lab, equipment_type, filtered_logs, summary, insights)
333
- # Display recent logs
334
- logs_output = "\n".join(log_messages[-10:]) or "No logs available."
335
- logger.info("Dashboard rendered successfully")
336
- return (
337
- device_cards,
338
- fig_usage,
339
- fig_uptime,
340
- anomaly_table,
341
- pdf_data,
342
- summary,
343
- insights,
344
- logs_output
345
- )
346
- except Exception as e:
347
- logger.error(f"Error rendering dashboard: {e}")
348
- logs_output = "\n".join(log_messages[-10:]) or "No logs available."
349
- return (
350
- f"<p style='color: red;'>Error rendering dashboard: {str(e)}</p>",
351
- None,
352
- None,
353
- "<p style='color: red;'>Error</p>",
354
- None,
355
- "Error generating summary.",
356
- "Error generating insights.",
357
- logs_output
358
- )
359
-
360
- # Define Gradio interface
361
- with gr.Blocks(
362
- css="""
363
- .gradio-container {max-width: 1200px; margin: auto; padding: 20px;}
364
- .table {width: 100%; border-collapse: collapse;}
365
- .table th, .table td {padding: 8px; text-align: left;}
366
- .table-striped tbody tr:nth-of-type(odd) {background-color: #f9f9f9;}
367
- h1 {color: #2c3e50; text-align: center;}
368
- .submit-btn {margin-top: 10px;}
369
- """
370
- ) as demo:
371
- gr.Markdown("## LabOps Central Dashboard")
372
-
373
- # File Upload Section
374
- with gr.Group():
375
- gr.Markdown("### Upload Data File")
376
- gr.Markdown("**Note**: Please upload a logs.csv file to proceed.")
377
- logs_file = gr.File(label="Upload Logs CSV (Required)", file_types=[".csv"])
378
- upload_btn = gr.Button("Load Data", variant="primary", elem_classes="submit-btn")
379
- upload_status = gr.HTML(label="Upload Status")
380
-
381
- # Filter Options
382
- with gr.Group():
383
- gr.Markdown("### Filter Options")
384
- with gr.Row():
385
- lab = gr.Dropdown(
386
- choices=[],
387
- label="Select Lab",
388
- value=None,
389
- interactive=True
390
- )
391
- equipment_type = gr.Dropdown(
392
- choices=[],
393
- label="Select Equipment Type",
394
- value=None,
395
- interactive=True
396
- )
397
- with gr.Row():
398
- start_date = gr.Textbox(
399
- label="Start Date (YYYY-MM-DD)",
400
- value="2025-05-01",
401
- placeholder="YYYY-MM-DD",
402
- info="Enter date in YYYY-MM-DD format (e.g., 2025-05-01)"
403
- )
404
- end_date = gr.Textbox(
405
- label="End Date (YYYY-MM-DD)",
406
- value="2025-05-30",
407
- placeholder="YYYY-MM-DD",
408
- info="Enter date in YYYY-MM-DD format (e.g., 2025-05-30)"
409
- )
410
- submit_btn = gr.Button("Generate Dashboard", variant="primary", elem_classes="submit-btn", interactive=False)
411
-
412
- # Output Section
413
- with gr.Group():
414
- gr.Markdown("### Dashboard Results")
415
- with gr.Tabs():
416
- with gr.Tab("Device Status"):
417
- device_cards = gr.HTML(label="Device Status")
418
- with gr.Tab("Usage Trends"):
419
- usage_chart = gr.Plot(label="Usage Trends")
420
- with gr.Tab("Uptime"):
421
- uptime_chart = gr.Plot(label="Weekly Uptime %")
422
- with gr.Tab("Anomaly Alerts"):
423
- anomaly_table = gr.HTML(label="Anomaly Alerts")
424
- with gr.Tab("Summary"):
425
- summary_output = gr.Textbox(label="Log Summary", lines=5, interactive=False)
426
- with gr.Tab("Executive Insights"):
427
- insights_output = gr.Textbox(label="Executive Insights", lines=5, interactive=False)
428
- pdf_download = gr.File(label="Download PDF Report")
429
- log_display = gr.Textbox(label="Debug Logs", lines=10, interactive=False)
430
-
431
- # Connect upload button to dropdown update function
432
- upload_btn.click(
433
- fn=update_dropdowns,
434
- inputs=[logs_file],
435
- outputs=[lab, equipment_type, upload_status, submit_btn]
436
- )
437
- # Connect submit button to dashboard rendering
438
- inputs = [lab, equipment_type, start_date, end_date]
439
- outputs = [device_cards, usage_chart, uptime_chart, anomaly_table, pdf_download, summary_output, insights_output, log_display]
440
- submit_btn.click(render_dashboard, inputs=inputs, outputs=outputs)
441
-
442
- # Launch Gradio app
443
- if __name__ == "__main__":
444
- try:
445
- demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
446
- except Exception as e:
447
- logger.error(f"Failed to launch Gradio app: {e}")
 
1
  import gradio as gr
2
  import pandas as pd
3
+ import matplotlib.pyplot as plt
4
+ import io
5
+ from datetime import datetime
6
+ from fpdf import FPDF
7
+
8
+ # ----------------------------
9
+ # Function: Load Logs from Uploaded File or Default
10
+ # ----------------------------
11
+ def load_logs(file_obj):
12
+ if file_obj is not None:
13
+ df = pd.read_csv(file_obj.name, parse_dates=['Timestamp'])
14
+ else:
15
+ # Sample fallback data
16
+ sample_data = {
17
+ 'DeviceID': ['D001', 'D002', 'D003'],
18
+ 'Lab': ['Lab A', 'Lab B', 'Lab A'],
19
+ 'Type': ['UV', 'Weight', 'Cell'],
20
+ 'Timestamp': [pd.Timestamp('2025-06-01 09:00:00'),
21
+ pd.Timestamp('2025-06-01 10:00:00'),
22
+ pd.Timestamp('2025-06-01 11:00:00')],
23
+ 'Status': ['OK', 'DOWN', 'OK'],
24
+ 'UsageCount': [120, 85, 100]
25
+ }
26
+ df = pd.DataFrame(sample_data)
27
+ return df
28
+
29
+ # ----------------------------
30
+ # Function: Summarize Log Data
31
+ # ----------------------------
32
+ def summarize_logs(df):
33
+ summary = df.groupby(['Lab', 'Type'])['Status'].value_counts().unstack().fillna(0)
34
+ return summary
35
+
36
+ # ----------------------------
37
+ # Function: Generate Chart from Summary
38
+ # ----------------------------
39
+ def generate_chart(df):
40
+ summary = summarize_logs(df)
41
+ fig, ax = plt.subplots(figsize=(8, 4))
42
+ summary.plot(kind='bar', stacked=True, ax=ax)
43
+ ax.set_title("Device Uptime/Downtime Summary")
44
+ ax.set_ylabel("Count")
45
+ ax.set_xlabel("Lab + Device Type")
46
+ ax.legend(title="Status")
47
+ fig.tight_layout()
48
+ return fig
49
+
50
+ # ----------------------------
51
+ # Function: Export Summary to PDF
52
+ # ----------------------------
53
+ def export_pdf(df):
54
+ summary = summarize_logs(df)
55
+ pdf = FPDF()
56
+ pdf.add_page()
57
+ pdf.set_font("Arial", size=12)
58
+
59
+ pdf.cell(200, 10, txt="LabOps Dashboard Summary Report", ln=True, align='C')
60
+ pdf.cell(200, 10, txt=f"Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", ln=True, align='C')
61
+ pdf.ln(10)
62
+
63
+ # Table header
64
+ headers = ["Lab", "Device", "OK", "DOWN"]
65
+ col_widths = [40, 40, 30, 30]
66
+ for header, width in zip(headers, col_widths):
67
+ pdf.cell(width, 10, header, border=1)
68
+ pdf.ln()
69
+
70
+ # Table rows
71
+ for (lab, dev_type), row in summary.iterrows():
72
+ ok = int(row.get('OK', 0))
73
+ down = int(row.get('DOWN', 0))
74
+ pdf.cell(40, 10, lab, border=1)
75
+ pdf.cell(40, 10, dev_type, border=1)
76
+ pdf.cell(30, 10, str(ok), border=1)
77
+ pdf.cell(30, 10, str(down), border=1)
78
+ pdf.ln()
79
+
80
+ # Return PDF bytes
81
+ output = io.BytesIO()
82
+ pdf.output(output)
83
+ output.seek(0)
84
+ return output
85
+
86
+ # ----------------------------
87
+ # Gradio UI
88
+ # ----------------------------
89
+ def dashboard(file_obj):
90
+ df = load_logs(file_obj)
91
+ fig = generate_chart(df)
92
+ return fig
93
+
94
+ def generate_pdf_button(file_obj):
95
+ df = load_logs(file_obj)
96
+ pdf_bytes = export_pdf(df)
97
+ return gr.File.update(value=pdf_bytes, visible=True)
98
+
99
+ with gr.Blocks() as demo:
100
+ gr.Markdown("## 🧪 LabOps Dashboard")
101
+ gr.Markdown("Monitor and analyze device logs across SmartLabs.")
102
+
103
+ with gr.Row():
104
+ file_input = gr.File(label="Upload Log CSV (Optional)", file_types=[".csv"])
105
+ download_button = gr.Button("Download PDF Summary")
106
+ download_file = gr.File(label="Download PDF", visible=False)
107
+
108
+ plot_output = gr.Plot()
109
+
110
+ file_input.change(fn=dashboard, inputs=file_input, outputs=plot_output)
111
+ download_button.click(fn=generate_pdf_button, inputs=file_input, outputs=download_file)
112
+
113
+ demo.launch()