AYI-NEDJIMI commited on
Commit
1e4cc58
·
verified ·
1 Parent(s): 3d20b32

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +551 -0
app.py ADDED
@@ -0,0 +1,551 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ import plotly.graph_objects as go
5
+ from datasets import load_dataset
6
+ import json
7
+ from typing import Optional, Dict, List
8
+ import warnings
9
+
10
+ warnings.filterwarnings("ignore")
11
+
12
+ # Global data cache
13
+ data_cache = {
14
+ "fr": {},
15
+ "en": {}
16
+ }
17
+
18
+ def load_datasets_for_language(lang: str) -> Dict:
19
+ """Load all datasets for a specific language."""
20
+ if data_cache[lang]:
21
+ return data_cache[lang]
22
+
23
+ dataset_name = "AYI-NEDJIMI/ad-attacks-fr" if lang == "fr" else "AYI-NEDJIMI/ad-attacks-en"
24
+
25
+ try:
26
+ attacks_dataset = load_dataset(dataset_name, data_files="attacks.json", split="train")
27
+ tools_dataset = load_dataset(dataset_name, data_files="tools.json", split="train")
28
+ rules_dataset = load_dataset(dataset_name, data_files="detection_rules.json", split="train")
29
+ killchains_dataset = load_dataset(dataset_name, data_files="killchains.json", split="train")
30
+ qa_dataset = load_dataset(dataset_name, data_files="qa_dataset.json", split="train")
31
+
32
+ data_cache[lang] = {
33
+ "attacks": pd.DataFrame(attacks_dataset),
34
+ "tools": pd.DataFrame(tools_dataset),
35
+ "rules": pd.DataFrame(rules_dataset),
36
+ "killchains": pd.DataFrame(killchains_dataset),
37
+ "qa": pd.DataFrame(qa_dataset)
38
+ }
39
+ except Exception as e:
40
+ print(f"Error loading dataset for {lang}: {e}")
41
+ data_cache[lang] = {
42
+ "attacks": pd.DataFrame(),
43
+ "tools": pd.DataFrame(),
44
+ "rules": pd.DataFrame(),
45
+ "killchains": pd.DataFrame(),
46
+ "qa": pd.DataFrame()
47
+ }
48
+
49
+ return data_cache[lang]
50
+
51
+ def convert_list_to_string(val) -> str:
52
+ """Convert list or dict to readable string for display."""
53
+ if isinstance(val, list):
54
+ return ", ".join([str(v) for v in val])
55
+ elif isinstance(val, dict):
56
+ return json.dumps(val, ensure_ascii=False, indent=2)
57
+ return str(val) if val else ""
58
+
59
+ def prepare_attacks_df(df: pd.DataFrame) -> pd.DataFrame:
60
+ """Prepare attacks dataframe for display."""
61
+ if df.empty:
62
+ return df
63
+ df = df.copy()
64
+ for col in ["mitre_technique_ids", "tools", "command_examples"]:
65
+ if col in df.columns:
66
+ df[col] = df[col].apply(convert_list_to_string)
67
+ return df
68
+
69
+ def prepare_tools_df(df: pd.DataFrame) -> pd.DataFrame:
70
+ """Prepare tools dataframe for display."""
71
+ if df.empty:
72
+ return df
73
+ df = df.copy()
74
+ if "attacks_related" in df.columns:
75
+ df["attacks_related"] = df["attacks_related"].apply(convert_list_to_string)
76
+ if "platforms" in df.columns:
77
+ df["platforms"] = df["platforms"].apply(convert_list_to_string)
78
+ return df
79
+
80
+ def prepare_rules_df(df: pd.DataFrame) -> pd.DataFrame:
81
+ """Prepare detection rules dataframe for display."""
82
+ if df.empty:
83
+ return df
84
+ df = df.copy()
85
+ if "event_ids" in df.columns:
86
+ df["event_ids"] = df["event_ids"].apply(convert_list_to_string)
87
+ if "attacks_related" in df.columns:
88
+ df["attacks_related"] = df["attacks_related"].apply(convert_list_to_string)
89
+ return df
90
+
91
+ def prepare_qa_df(df: pd.DataFrame) -> pd.DataFrame:
92
+ """Prepare Q&A dataframe for display."""
93
+ if df.empty:
94
+ return df
95
+ df = df.copy()
96
+ if "keywords" in df.columns:
97
+ df["keywords"] = df["keywords"].apply(convert_list_to_string)
98
+ return df
99
+
100
+ def filter_dataframe(df: pd.DataFrame, search_text: str, filter_col: Optional[str] = None, filter_value: Optional[str] = None) -> pd.DataFrame:
101
+ """Filter dataframe by search text and optional category/filter."""
102
+ if df.empty:
103
+ return df
104
+
105
+ result = df.copy()
106
+
107
+ if search_text.strip():
108
+ search_lower = search_text.lower()
109
+ mask = result.astype(str).apply(lambda x: x.str.contains(search_lower, case=False)).any(axis=1)
110
+ result = result[mask]
111
+
112
+ if filter_col and filter_value and filter_value != "All":
113
+ if filter_col in result.columns:
114
+ result = result[result[filter_col] == filter_value]
115
+
116
+ return result
117
+
118
+ def get_unique_values(df: pd.DataFrame, column: str) -> List[str]:
119
+ """Get unique values from a column."""
120
+ if df.empty or column not in df.columns:
121
+ return []
122
+ return ["All"] + sorted(df[column].unique().astype(str).tolist())
123
+
124
+ def create_attacks_tab(lang_data: Dict) -> tuple:
125
+ """Create attacks tab content."""
126
+ df = lang_data["attacks"]
127
+
128
+ if df.empty:
129
+ return gr.DataFrame(value=pd.DataFrame()), [], "No data available"
130
+
131
+ categories = get_unique_values(df, "category")
132
+ severities = get_unique_values(df, "severity") if "severity" in df.columns else []
133
+
134
+ return prepare_attacks_df(df), categories, severities
135
+
136
+ def create_tools_tab(lang_data: Dict) -> tuple:
137
+ """Create tools tab content."""
138
+ df = lang_data["tools"]
139
+
140
+ if df.empty:
141
+ return gr.DataFrame(value=pd.DataFrame()), []
142
+
143
+ categories = get_unique_values(df, "category") if "category" in df.columns else []
144
+
145
+ return prepare_tools_df(df), categories
146
+
147
+ def create_rules_tab(lang_data: Dict) -> tuple:
148
+ """Create detection rules tab content."""
149
+ df = lang_data["rules"]
150
+
151
+ if df.empty:
152
+ return gr.DataFrame(value=pd.DataFrame()), []
153
+
154
+ log_sources = get_unique_values(df, "log_source") if "log_source" in df.columns else []
155
+
156
+ return prepare_rules_df(df), log_sources
157
+
158
+ def create_qa_tab(lang_data: Dict) -> tuple:
159
+ """Create Q&A tab content."""
160
+ df = lang_data["qa"]
161
+
162
+ if df.empty:
163
+ return gr.DataFrame(value=pd.DataFrame()), [], []
164
+
165
+ categories = get_unique_values(df, "category") if "category" in df.columns else []
166
+ difficulties = get_unique_values(df, "difficulty") if "difficulty" in df.columns else []
167
+
168
+ return prepare_qa_df(df), categories, difficulties
169
+
170
+ def create_statistics(lang_data: Dict, lang: str) -> tuple:
171
+ """Create statistics visualizations."""
172
+ df_attacks = lang_data["attacks"]
173
+
174
+ if df_attacks.empty:
175
+ empty_fig = go.Figure()
176
+ empty_fig.add_annotation(text="No data available")
177
+ return empty_fig, empty_fig, empty_fig, "No statistics available"
178
+
179
+ # Attacks per category
180
+ if "category" in df_attacks.columns:
181
+ category_counts = df_attacks["category"].value_counts().reset_index()
182
+ category_counts.columns = ["category", "count"]
183
+ fig_category = px.bar(
184
+ category_counts,
185
+ x="category",
186
+ y="count",
187
+ title="Attacks per Category" if lang == "en" else "Attaques par Catégorie",
188
+ labels={"category": "Category", "count": "Count"} if lang == "en" else {"category": "Catégorie", "count": "Nombre"}
189
+ )
190
+ else:
191
+ fig_category = go.Figure()
192
+ fig_category.add_annotation(text="Category data not available")
193
+
194
+ # Severity distribution
195
+ if "severity" in df_attacks.columns:
196
+ severity_counts = df_attacks["severity"].value_counts().reset_index()
197
+ severity_counts.columns = ["severity", "count"]
198
+ fig_severity = px.pie(
199
+ severity_counts,
200
+ names="severity",
201
+ values="count",
202
+ title="Severity Distribution" if lang == "en" else "Distribution de Sévérité"
203
+ )
204
+ else:
205
+ fig_severity = go.Figure()
206
+ fig_severity.add_annotation(text="Severity data not available")
207
+
208
+ # Tools usage
209
+ tools_list = []
210
+ if "tools" in df_attacks.columns:
211
+ for tools in df_attacks["tools"]:
212
+ if isinstance(tools, list):
213
+ tools_list.extend(tools)
214
+
215
+ if tools_list:
216
+ tools_df = pd.Series(tools_list).value_counts().reset_index()
217
+ tools_df.columns = ["tool", "count"]
218
+ tools_df = tools_df.head(10)
219
+ fig_tools = px.bar(
220
+ tools_df,
221
+ x="tool",
222
+ y="count",
223
+ title="Most Used Tools (Top 10)" if lang == "en" else "Outils les Plus Utilisés (Top 10)",
224
+ labels={"tool": "Tool", "count": "Count"} if lang == "en" else {"tool": "Outil", "count": "Nombre"}
225
+ )
226
+ else:
227
+ fig_tools = go.Figure()
228
+ fig_tools.add_annotation(text="Tools data not available")
229
+
230
+ stats_text = f"Total Attacks: {len(df_attacks)}" if lang == "en" else f"Attaques Totales: {len(df_attacks)}"
231
+
232
+ return fig_category, fig_severity, fig_tools, stats_text
233
+
234
+ def update_on_language_change(language: str):
235
+ """Update all components when language changes."""
236
+ lang_data = load_datasets_for_language(language)
237
+
238
+ attacks_df, categories, severities = create_attacks_tab(lang_data)
239
+ tools_df, tools_cats = create_tools_tab(lang_data)
240
+ rules_df, log_sources = create_rules_tab(lang_data)
241
+ qa_df, qa_cats, qa_diffs = create_qa_tab(lang_data)
242
+ fig_cat, fig_sev, fig_tools, stats_text = create_statistics(lang_data, language)
243
+
244
+ return (
245
+ attacks_df,
246
+ gr.Dropdown(choices=categories, value="All"),
247
+ gr.Dropdown(choices=severities, value="All"),
248
+ tools_df,
249
+ gr.Dropdown(choices=tools_cats, value="All"),
250
+ rules_df,
251
+ gr.Dropdown(choices=log_sources, value="All"),
252
+ qa_df,
253
+ gr.Dropdown(choices=qa_cats, value="All"),
254
+ gr.Dropdown(choices=qa_diffs, value="All"),
255
+ fig_cat,
256
+ fig_sev,
257
+ fig_tools,
258
+ stats_text
259
+ )
260
+
261
+ # Load initial data
262
+ initial_lang = "en"
263
+ initial_data = load_datasets_for_language(initial_lang)
264
+
265
+ # Create Gradio app
266
+ with gr.Blocks(title="AD Attack Explorer", theme=gr.themes.Soft()) as demo:
267
+ gr.Markdown("# 🏰 AD Attack Explorer")
268
+ gr.Markdown("Interactive exploration of Active Directory attacks, tools, detection rules, kill chains, and Q&A datasets")
269
+
270
+ with gr.Row():
271
+ language = gr.Radio(
272
+ choices=["English", "Français"],
273
+ value="English",
274
+ label="Language / Langue",
275
+ scale=1
276
+ )
277
+
278
+ # Create tabs
279
+ with gr.Tabs():
280
+ # Attacks Tab
281
+ with gr.TabItem("Attacks / Attaques"):
282
+ with gr.Row():
283
+ search_attacks = gr.Textbox(
284
+ label="Search / Rechercher",
285
+ placeholder="Search attacks...",
286
+ scale=2
287
+ )
288
+ with gr.Row():
289
+ filter_category = gr.Dropdown(
290
+ choices=get_unique_values(initial_data["attacks"], "category"),
291
+ value="All",
292
+ label="Category / Catégorie",
293
+ scale=1
294
+ )
295
+ filter_severity = gr.Dropdown(
296
+ choices=get_unique_values(initial_data["attacks"], "severity") if "severity" in initial_data["attacks"].columns else [],
297
+ value="All",
298
+ label="Severity / Sévérité",
299
+ scale=1
300
+ )
301
+
302
+ attacks_table = gr.Dataframe(
303
+ value=prepare_attacks_df(initial_data["attacks"]),
304
+ interactive=False,
305
+ scale=2
306
+ )
307
+
308
+ # Tools Tab
309
+ with gr.TabItem("Tools / Outils"):
310
+ with gr.Row():
311
+ search_tools = gr.Textbox(
312
+ label="Search / Rechercher",
313
+ placeholder="Search tools...",
314
+ scale=2
315
+ )
316
+ with gr.Row():
317
+ filter_tools_cat = gr.Dropdown(
318
+ choices=get_unique_values(initial_data["tools"], "category") if "category" in initial_data["tools"].columns else [],
319
+ value="All",
320
+ label="Category / Catégorie",
321
+ scale=1
322
+ )
323
+
324
+ tools_table = gr.Dataframe(
325
+ value=prepare_tools_df(initial_data["tools"]),
326
+ interactive=False,
327
+ scale=2
328
+ )
329
+
330
+ # Detection Rules Tab
331
+ with gr.TabItem("Detection Rules / Règles Détection"):
332
+ with gr.Row():
333
+ search_rules = gr.Textbox(
334
+ label="Search / Rechercher",
335
+ placeholder="Search rules...",
336
+ scale=2
337
+ )
338
+ with gr.Row():
339
+ filter_rules_log = gr.Dropdown(
340
+ choices=get_unique_values(initial_data["rules"], "log_source") if "log_source" in initial_data["rules"].columns else [],
341
+ value="All",
342
+ label="Log Source",
343
+ scale=1
344
+ )
345
+
346
+ rules_table = gr.Dataframe(
347
+ value=prepare_rules_df(initial_data["rules"]),
348
+ interactive=False,
349
+ scale=2
350
+ )
351
+
352
+ # Kill Chains Tab
353
+ with gr.TabItem("Kill Chains"):
354
+ with gr.Row():
355
+ search_killchains = gr.Textbox(
356
+ label="Search / Rechercher",
357
+ placeholder="Search kill chains...",
358
+ scale=2
359
+ )
360
+
361
+ killchains_table = gr.Dataframe(
362
+ value=initial_data["killchains"],
363
+ interactive=False,
364
+ scale=2
365
+ )
366
+
367
+ # Q&A Tab
368
+ with gr.TabItem("Q&A"):
369
+ with gr.Row():
370
+ search_qa = gr.Textbox(
371
+ label="Search / Rechercher",
372
+ placeholder="Search questions...",
373
+ scale=2
374
+ )
375
+ with gr.Row():
376
+ filter_qa_cat = gr.Dropdown(
377
+ choices=get_unique_values(initial_data["qa"], "category") if "category" in initial_data["qa"].columns else [],
378
+ value="All",
379
+ label="Category / Catégorie",
380
+ scale=1
381
+ )
382
+ filter_qa_diff = gr.Dropdown(
383
+ choices=get_unique_values(initial_data["qa"], "difficulty") if "difficulty" in initial_data["qa"].columns else [],
384
+ value="All",
385
+ label="Difficulty / Difficulté",
386
+ scale=1
387
+ )
388
+
389
+ qa_table = gr.Dataframe(
390
+ value=prepare_qa_df(initial_data["qa"]),
391
+ interactive=False,
392
+ scale=2
393
+ )
394
+
395
+ # Statistics Tab
396
+ with gr.TabItem("Statistics / Statistiques"):
397
+ gr.Markdown("### Attack Analytics")
398
+
399
+ with gr.Row():
400
+ fig_cat, fig_sev, fig_tools, stats_text = create_statistics(initial_data, initial_lang)
401
+
402
+ with gr.Column():
403
+ stats_info = gr.Markdown(stats_text)
404
+
405
+ with gr.Row():
406
+ chart_category = gr.Plot(value=fig_cat, scale=1)
407
+ chart_severity = gr.Plot(value=fig_sev, scale=1)
408
+
409
+ with gr.Row():
410
+ chart_tools = gr.Plot(value=fig_tools, scale=2)
411
+
412
+ # Footer
413
+ gr.HTML("""
414
+ <div style='text-align:center; padding:20px; color:#666; margin-top:20px;'>
415
+ <p>Created by <a href='https://www.ayinedjimi-consultants.fr' target='_blank'>Ayi NEDJIMI</a> - Senior Offensive Cybersecurity & AI Consultant</p>
416
+ <p><a href='https://www.linkedin.com/in/ayi-nedjimi' target='_blank'>LinkedIn</a> | <a href='https://github.com/ayinedjimi' target='_blank'>GitHub</a> | <a href='https://x.com/AyiNEDJIMI' target='_blank'>Twitter/X</a></p>
417
+ </div>
418
+ """)
419
+
420
+ # Language change handlers
421
+ def on_language_change(language: str):
422
+ lang = "fr" if language == "Français" else "en"
423
+ return update_on_language_change(lang)
424
+
425
+ def update_attacks_display(search, category, severity, language):
426
+ lang = "fr" if language == "Français" else "en"
427
+ lang_data = load_datasets_for_language(lang)
428
+ df = lang_data["attacks"].copy()
429
+ df = filter_dataframe(df, search, "category" if category != "All" else None, category)
430
+ df = filter_dataframe(df, "", "severity" if severity != "All" else None, severity)
431
+ return prepare_attacks_df(df)
432
+
433
+ def update_tools_display(search, category, language):
434
+ lang = "fr" if language == "Français" else "en"
435
+ lang_data = load_datasets_for_language(lang)
436
+ df = lang_data["tools"].copy()
437
+ df = filter_dataframe(df, search, "category" if category != "All" else None, category)
438
+ return prepare_tools_df(df)
439
+
440
+ def update_rules_display(search, log_source, language):
441
+ lang = "fr" if language == "Français" else "en"
442
+ lang_data = load_datasets_for_language(lang)
443
+ df = lang_data["rules"].copy()
444
+ df = filter_dataframe(df, search, "log_source" if log_source != "All" else None, log_source)
445
+ return prepare_rules_df(df)
446
+
447
+ def update_killchains_display(search, language):
448
+ lang = "fr" if language == "Français" else "en"
449
+ lang_data = load_datasets_for_language(lang)
450
+ df = lang_data["killchains"].copy()
451
+ df = filter_dataframe(df, search)
452
+ return df
453
+
454
+ def update_qa_display(search, category, difficulty, language):
455
+ lang = "fr" if language == "Français" else "en"
456
+ lang_data = load_datasets_for_language(lang)
457
+ df = lang_data["qa"].copy()
458
+ df = filter_dataframe(df, search, "category" if category != "All" else None, category)
459
+ df = filter_dataframe(df, "", "difficulty" if difficulty != "All" else None, difficulty)
460
+ return prepare_qa_df(df)
461
+
462
+ # Connect event handlers
463
+ language.change(
464
+ on_language_change,
465
+ inputs=[language],
466
+ outputs=[
467
+ attacks_table,
468
+ filter_category,
469
+ filter_severity,
470
+ tools_table,
471
+ filter_tools_cat,
472
+ rules_table,
473
+ filter_rules_log,
474
+ qa_table,
475
+ filter_qa_cat,
476
+ filter_qa_diff,
477
+ chart_category,
478
+ chart_severity,
479
+ chart_tools,
480
+ stats_info
481
+ ]
482
+ )
483
+
484
+ search_attacks.change(
485
+ update_attacks_display,
486
+ inputs=[search_attacks, filter_category, filter_severity, language],
487
+ outputs=[attacks_table]
488
+ )
489
+
490
+ filter_category.change(
491
+ update_attacks_display,
492
+ inputs=[search_attacks, filter_category, filter_severity, language],
493
+ outputs=[attacks_table]
494
+ )
495
+
496
+ filter_severity.change(
497
+ update_attacks_display,
498
+ inputs=[search_attacks, filter_category, filter_severity, language],
499
+ outputs=[attacks_table]
500
+ )
501
+
502
+ search_tools.change(
503
+ update_tools_display,
504
+ inputs=[search_tools, filter_tools_cat, language],
505
+ outputs=[tools_table]
506
+ )
507
+
508
+ filter_tools_cat.change(
509
+ update_tools_display,
510
+ inputs=[search_tools, filter_tools_cat, language],
511
+ outputs=[tools_table]
512
+ )
513
+
514
+ search_rules.change(
515
+ update_rules_display,
516
+ inputs=[search_rules, filter_rules_log, language],
517
+ outputs=[rules_table]
518
+ )
519
+
520
+ filter_rules_log.change(
521
+ update_rules_display,
522
+ inputs=[search_rules, filter_rules_log, language],
523
+ outputs=[rules_table]
524
+ )
525
+
526
+ search_killchains.change(
527
+ update_killchains_display,
528
+ inputs=[search_killchains, language],
529
+ outputs=[killchains_table]
530
+ )
531
+
532
+ search_qa.change(
533
+ update_qa_display,
534
+ inputs=[search_qa, filter_qa_cat, filter_qa_diff, language],
535
+ outputs=[qa_table]
536
+ )
537
+
538
+ filter_qa_cat.change(
539
+ update_qa_display,
540
+ inputs=[search_qa, filter_qa_cat, filter_qa_diff, language],
541
+ outputs=[qa_table]
542
+ )
543
+
544
+ filter_qa_diff.change(
545
+ update_qa_display,
546
+ inputs=[search_qa, filter_qa_cat, filter_qa_diff, language],
547
+ outputs=[qa_table]
548
+ )
549
+
550
+ if __name__ == "__main__":
551
+ demo.launch()