storresbusquets commited on
Commit
e48aa5d
1 Parent(s): 6b0b7f4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +462 -58
app.py CHANGED
@@ -1,76 +1,480 @@
 
1
  import gradio as gr
 
 
 
2
  import torch
 
 
 
3
  import transformers
4
  from langchain.llms import CTransformers
5
  from langchain import PromptTemplate, LLMChain
6
  from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
7
 
8
- # model = AutoModelForCausalLM.from_pretrained("marella/gpt-2-ggml", hf=True)
9
- # tokenizer = AutoTokenizer.from_pretrained(model)
10
 
11
- # access_token = os.getenv("Llama2")
 
 
 
 
12
 
13
- def greet(text):
 
 
 
 
 
 
 
 
 
 
14
 
15
- llm = CTransformers(model="TheBloke/Llama-2-7B-Chat-GGML", model_file = 'llama-2-7b-chat.ggmlv3.q2_K.bin', callbacks=[StreamingStdOutCallbackHandler()])
16
-
17
- template = """
18
- [INST] <<SYS>>
19
- You are a helpful, respectful and honest assistant that performs summaries of text. Write a concise summary of the following text.
20
- <</SYS>>
21
- {text}[/INST]
22
- """
23
-
24
- prompt = PromptTemplate(template=template, input_variables=["text"])
25
- llm_chain = LLMChain(prompt=prompt, llm=llm)
26
- summary = llm_chain.run(text)
 
 
 
 
 
 
 
 
27
 
28
- return summary
29
-
30
-
31
-
32
-
33
- # model = AutoModelForCausalLM.from_pretrained("TheBloke/Llama-2-7B-Chat-GGML", model_file = 'llama-2-7b-chat.ggmlv3.q4_K_S.bin', hf=True)
34
- # tokenizer = AutoTokenizer.from_pretrained(model)
35
-
36
- # model = "meta-llama/Llama-2-7b-hf"
37
- # tokenizer = AutoTokenizer.from_pretrained(model, token=access_token)
38
 
39
- # pipeline = transformers.pipeline(
40
- # "text-generation",
41
- # model=model,
42
- # tokenizer=tokenizer,
43
- # torch_dtype=torch.bfloat16,
44
- # trust_remote_code=True,
45
- # device_map="auto",
46
- # max_length=512,
47
- # max_new_tokens=256,
48
- # do_sample=True,
49
- # top_k=10,
50
- # num_return_sequences=1,
51
- # eos_token_id=tokenizer.eos_token_id,
52
- # token=access_token
53
- # )
54
-
55
- # llm = HuggingFacePipeline(pipeline = pipeline, model_kwargs = {'temperature':0,'repetition_penalty':1.1})
56
-
57
- # template = """Write a concise summary of the following:
58
- # "{text}"
59
- # CONCISE SUMMARY:"""
60
-
61
- # prompt = PromptTemplate(template=template, input_variables=["text"])
62
- # llm_chain = LLMChain(prompt=prompt, llm=llm)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
- # return llm_chain.run(text)
 
 
 
 
 
 
 
 
 
 
65
 
66
- with gr.Blocks() as demo:
 
 
 
67
 
68
- text = gr.Textbox(label="Text")
69
- summary = gr.Textbox(label="Summary")
70
- greet_btn = gr.Button("Submit")
71
- clear = gr.ClearButton([text, summary])
72
- greet_btn.click(fn=greet, inputs=text, outputs=summary, api_name="greet")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
 
75
 
76
- demo.launch()
 
1
+ # Imports
2
  import gradio as gr
3
+ import whisper
4
+ from pytube import YouTube
5
+ from transformers import pipeline, T5Tokenizer, T5ForConditionalGeneration, AutoTokenizer, AutoModelForSeq2SeqLM
6
  import torch
7
+ from wordcloud import WordCloud
8
+ import re
9
+ import os
10
  import transformers
11
  from langchain.llms import CTransformers
12
  from langchain import PromptTemplate, LLMChain
13
  from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
14
 
 
 
15
 
16
+ class GradioInference:
17
+ def __init__(self):
18
+
19
+ # OpenAI's Whisper model sizes
20
+ self.sizes = list(whisper._MODELS.keys())
21
 
22
+ # Whisper's available languages for ASR
23
+ self.langs = ["none"] + sorted(list(whisper.tokenizer.LANGUAGES.values()))
24
+
25
+ # Default size
26
+ self.current_size = "base"
27
+
28
+ # Default model size
29
+ self.loaded_model = whisper.load_model(self.current_size)
30
+
31
+ # Initialize Pytube Object
32
+ self.yt = None
33
 
34
+ # Initialize summary model for English
35
+ self.summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
36
+
37
+ # Initialize VoiceLabT5 model and tokenizer
38
+ self.keyword_model = T5ForConditionalGeneration.from_pretrained(
39
+ "Voicelab/vlt5-base-keywords"
40
+ )
41
+ self.keyword_tokenizer = T5Tokenizer.from_pretrained(
42
+ "Voicelab/vlt5-base-keywords"
43
+ )
44
+
45
+ # Sentiment Classifier
46
+ self.classifier = pipeline("text-classification", model="lxyuan/distilbert-base-multilingual-cased-sentiments-student", return_all_scores=False)
47
+
48
+ # Initialize Multilingual summary model
49
+ self.tokenizer = AutoTokenizer.from_pretrained("csebuetnlp/mT5_multilingual_XLSum")
50
+ self.model = AutoModelForSeq2SeqLM.from_pretrained("csebuetnlp/mT5_multilingual_XLSum")
51
+
52
+
53
+ self.llm = CTransformers(model="TheBloke/Llama-2-7B-Chat-GGML", model_file = 'llama-2-7b-chat.ggmlv3.q2_K.bin', callbacks=[StreamingStdOutCallbackHandler()])
54
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ def __call__(self, link, lang, size, progress=gr.Progress()):
57
+ """
58
+ Call the Gradio Inference python class.
59
+ This class gets access to a YouTube video using python's library Pytube and downloads its audio.
60
+ Then it uses the Whisper model to perform Automatic Speech Recognition (i.e Speech-to-Text).
61
+ Once the function has the transcription of the video it proccess it to obtain:
62
+ - Summary: using Facebook's BART transformer.
63
+ - KeyWords: using VoiceLabT5 keyword extractor.
64
+ - Sentiment Analysis: using Hugging Face's default sentiment classifier
65
+ - WordCloud: using the wordcloud python library.
66
+ """
67
+ progress(0, desc="Starting analysis")
68
+ if self.yt is None:
69
+ self.yt = YouTube(link)
70
+
71
+ # Pytube library to access to YouTube audio stream
72
+ path = self.yt.streams.filter(only_audio=True)[0].download(filename="tmp.mp4")
73
+
74
+ if lang == "none":
75
+ lang = None
76
+
77
+ if size != self.current_size:
78
+ self.loaded_model = whisper.load_model(size)
79
+ self.current_size = size
80
+
81
+ progress(0.20, desc="Transcribing")
82
+
83
+ # Transcribe the audio extracted from pytube
84
+ results = self.loaded_model.transcribe(path, language=lang)
85
+
86
+ progress(0.40, desc="Summarizing")
87
+
88
+ # Perform summarization on the transcription
89
+ # transcription_summary = self.summarizer(
90
+ # results["text"], max_length=150, min_length=30, do_sample=False
91
+ # )
92
+
93
+ #### Prueba
94
+ # WHITESPACE_HANDLER = lambda k: re.sub('\s+', ' ', re.sub('\n+', ' ', k.strip()))
95
+
96
+ # input_ids_sum = self.tokenizer(
97
+ # [WHITESPACE_HANDLER(results["text"])],
98
+ # return_tensors="pt",
99
+ # padding="max_length",
100
+ # truncation=True,
101
+ # max_length=512
102
+ # )["input_ids"]
103
+
104
+ # output_ids_sum = self.model.generate(
105
+ # input_ids=input_ids_sum,
106
+ # max_length=130,
107
+ # no_repeat_ngram_size=2,
108
+ # num_beams=4
109
+ # )[0]
110
+
111
+ # summary = self.tokenizer.decode(
112
+ # output_ids_sum,
113
+ # skip_special_tokens=True,
114
+ # clean_up_tokenization_spaces=False
115
+ # )
116
+ #### Fin prueba
117
+
118
+ ### Prueba con LLM ###
119
+
120
+ template = """
121
+ [INST] <<SYS>>
122
+ You are a helpful, respectful and honest assistant that performs summaries of text. Write a concise summary of the following text.
123
+ <</SYS>>
124
+ {text}[/INST]
125
+ """
126
+
127
+ prompt = PromptTemplate(template=template, input_variables=["text"])
128
+ llm_chain = LLMChain(prompt=prompt, llm=self.llm)
129
+ summary2 = llm_chain.run(results["text"])
130
+
131
+ ### Fin prueba LLM ###
132
+
133
+ progress(0.60, desc="Extracting Keywords")
134
+
135
+ # Extract keywords using VoiceLabT5
136
+ task_prefix = "Keywords: "
137
+ input_sequence = task_prefix + results["text"]
138
+ input_ids = self.keyword_tokenizer(
139
+ input_sequence, return_tensors="pt", truncation=False
140
+ ).input_ids
141
+ output = self.keyword_model.generate(
142
+ input_ids, no_repeat_ngram_size=3, num_beams=4
143
+ )
144
+ predicted = self.keyword_tokenizer.decode(output[0], skip_special_tokens=True)
145
+ keywords = [x.strip() for x in predicted.split(",") if x.strip()]
146
+ formatted_keywords = "\n".join([f"• {keyword}" for keyword in keywords])
147
+
148
+ progress(0.80, desc="Extracting Sentiment")
149
+
150
+ # Define a dictionary to map labels to emojis
151
+ sentiment_emojis = {
152
+ "positive": "Positive 👍🏼",
153
+ "negative": "Negative 👎🏼",
154
+ "neutral": "Neutral 😶",
155
+ }
156
+
157
+ # Sentiment label
158
+ label = self.classifier(summary)[0]["label"]
159
+
160
+ # Format the label with emojis
161
+ formatted_sentiment = sentiment_emojis.get(label, label)
162
+
163
+ progress(0.90, desc="Generating Wordcloud")
164
+
165
+ # Generate WordCloud object
166
+ wordcloud = WordCloud(colormap = "Oranges").generate(results["text"])
167
+
168
+ # WordCloud image to display
169
+ wordcloud_image = wordcloud.to_image()
170
+
171
+ if lang == "english":
172
+ return (
173
+ results["text"],
174
+ summary2,
175
+ # transcription_summary[0]["summary_text"],
176
+ formatted_keywords,
177
+ formatted_sentiment,
178
+ wordcloud_image,
179
+ )
180
+ else:
181
+ return (
182
+ results["text"],
183
+ summary2,
184
+ formatted_keywords,
185
+ formatted_sentiment,
186
+ wordcloud_image,
187
+ )
188
+
189
+
190
+ def populate_metadata(self, link):
191
+ """
192
+ Access to the YouTube video title and thumbnail image to further display it
193
+ params:
194
+ - link: a YouTube URL.
195
+ """
196
+ if not link:
197
+ return None, None
198
+
199
+ self.yt = YouTube(link)
200
+ return self.yt.thumbnail_url, self.yt.title
201
+
202
+ def from_audio_input(self, lang, size, audio_file, progress=gr.Progress()):
203
+ """
204
+ Call the Gradio Inference python class.
205
+ Uses it directly the Whisper model to perform Automatic Speech Recognition (i.e Speech-to-Text).
206
+ Once the function has the transcription of the video it proccess it to obtain:
207
+ - Summary: using Facebook's BART transformer.
208
+ - KeyWords: using VoiceLabT5 keyword extractor.
209
+ - Sentiment Analysis: using Hugging Face's default sentiment classifier
210
+ - WordCloud: using the wordcloud python library.
211
+ """
212
+ progress(0, desc="Starting analysis")
213
+
214
+ if lang == "none":
215
+ lang = None
216
+
217
+ if size != self.current_size:
218
+ self.loaded_model = whisper.load_model(size)
219
+ self.current_size = size
220
+
221
+ progress(0.20, desc="Transcribing")
222
+
223
+ results = self.loaded_model.transcribe(audio_file, language=lang)
224
+
225
+ progress(0.40, desc="Summarizing")
226
+
227
+ # Perform summarization on the transcription
228
+ transcription_summary = self.summarizer(
229
+ results["text"], max_length=150, min_length=30, do_sample=False
230
+ )
231
+
232
+ #### Prueba
233
+ WHITESPACE_HANDLER = lambda k: re.sub('\s+', ' ', re.sub('\n+', ' ', k.strip()))
234
+
235
+ input_ids_sum = self.tokenizer(
236
+ [WHITESPACE_HANDLER(results["text"])],
237
+ return_tensors="pt",
238
+ padding="max_length",
239
+ truncation=True,
240
+ max_length=512
241
+ )["input_ids"]
242
+
243
+ output_ids_sum = self.model.generate(
244
+ input_ids=input_ids_sum,
245
+ max_length=130,
246
+ no_repeat_ngram_size=2,
247
+ num_beams=4
248
+ )[0]
249
+
250
+ summary = self.tokenizer.decode(
251
+ output_ids_sum,
252
+ skip_special_tokens=True,
253
+ clean_up_tokenization_spaces=False
254
+ )
255
+ #### Fin prueba
256
+
257
+ progress(0.50, desc="Extracting Keywords")
258
+
259
+ # Extract keywords using VoiceLabT5
260
+ task_prefix = "Keywords: "
261
+ input_sequence = task_prefix + results["text"]
262
+ input_ids = self.keyword_tokenizer(
263
+ input_sequence, return_tensors="pt", truncation=False
264
+ ).input_ids
265
+ output = self.keyword_model.generate(
266
+ input_ids, no_repeat_ngram_size=3, num_beams=4
267
+ )
268
+ predicted = self.keyword_tokenizer.decode(output[0], skip_special_tokens=True)
269
+ keywords = [x.strip() for x in predicted.split(",") if x.strip()]
270
+ formatted_keywords = "\n".join([f"• {keyword}" for keyword in keywords])
271
+
272
+ progress(0.80, desc="Extracting Sentiment")
273
+
274
+ # Define a dictionary to map labels to emojis
275
+ sentiment_emojis = {
276
+ "positive": "Positive 👍🏼",
277
+ "negative": "Negative 👎🏼",
278
+ "neutral": "Neutral 😶",
279
+ }
280
+
281
+ # Sentiment label
282
+ label = self.classifier(summary)[0]["label"]
283
+
284
+ # Format the label with emojis
285
+ formatted_sentiment = sentiment_emojis.get(label, label)
286
+
287
+ progress(0.90, desc="Generating Wordcloud")
288
+ # WordCloud object
289
+ wordcloud = WordCloud(colormap = "Oranges").generate(
290
+ results["text"]
291
+ )
292
+ wordcloud_image = wordcloud.to_image()
293
+
294
+ if lang == "english":
295
+ return (
296
+ results["text"],
297
+ # summ,
298
+ transcription_summary[0]["summary_text"],
299
+ formatted_keywords,
300
+ formatted_sentiment,
301
+ wordcloud_image,
302
+ )
303
+ else:
304
+ return (
305
+ results["text"],
306
+ # summ,
307
+ summary,
308
+ formatted_keywords,
309
+ formatted_sentiment,
310
+ wordcloud_image,
311
+ )
312
+
313
+
314
+ gio = GradioInference()
315
+ title = "YouTube Insights"
316
+ description = "Your AI-powered video analytics tool"
317
+
318
+ block = gr.Blocks()
319
+
320
+ with block as demo:
321
+ gr.HTML(
322
+ """
323
+ <div style="text-align: center; max-width: 500px; margin: 0 auto;">
324
+ <div>
325
+ <h1>YouTube <span style="color: #FFA500;">Insights</span> 💡</h1>
326
+ </div>
327
+ <h4 style="margin-bottom: 10px; font-size: 95%">
328
+ Your AI-powered video analytics tool ✨
329
+ </h4>
330
+ </div>
331
+ """
332
+ )
333
+ with gr.Group():
334
+ with gr.Tab("From YouTube 📹"):
335
+ with gr.Box():
336
+
337
+ with gr.Row().style(equal_height=True):
338
+ size = gr.Dropdown(
339
+ label="Speech-to-text Model Size", choices=gio.sizes, value="base"
340
+ )
341
+ lang = gr.Dropdown(
342
+ label="Language (Optional)", choices=gio.langs, value="none"
343
+ )
344
+ link = gr.Textbox(
345
+ label="YouTube Link", placeholder="Enter YouTube link..."
346
+ )
347
+ title = gr.Label(label="Video Title")
348
+
349
+ with gr.Row().style(equal_height=True):
350
+ img = gr.Image(label="Thumbnail")
351
+ text = gr.Textbox(
352
+ label="Transcription",
353
+ placeholder="Transcription Output...",
354
+ lines=10,
355
+ ).style(show_copy_button=True, container=True)
356
+
357
+ with gr.Row().style(equal_height=True):
358
+ summary = gr.Textbox(
359
+ label="Summary", placeholder="Summary Output...", lines=5
360
+ ).style(show_copy_button=True, container=True)
361
+ keywords = gr.Textbox(
362
+ label="Keywords", placeholder="Keywords Output...", lines=5
363
+ ).style(show_copy_button=True, container=True)
364
+ label = gr.Label(label="Sentiment Analysis")
365
+ wordcloud_image = gr.Image(label="WordCloud")
366
+
367
+ with gr.Row().style(equal_height=True):
368
+ clear = gr.ClearButton(
369
+ [link, title, img, text, summary, keywords, label, wordcloud_image], scale=1, value="Clear 🗑️"
370
+ )
371
+ btn = gr.Button("Get video insights 🔎", variant="primary", scale=1)
372
+ btn.click(
373
+ gio,
374
+ inputs=[link, lang, size],
375
+ outputs=[text, summary, keywords, label, wordcloud_image],
376
+ )
377
+ link.change(gio.populate_metadata, inputs=[link], outputs=[img, title])
378
+
379
+ with gr.Tab("From Audio file 🎙️"):
380
+ with gr.Box():
381
+
382
+ with gr.Row().style(equal_height=True):
383
+ size = gr.Dropdown(
384
+ label="Model Size", choices=gio.sizes, value="base"
385
+ )
386
+ lang = gr.Dropdown(
387
+ label="Language (Optional)", choices=gio.langs, value="none"
388
+ )
389
+ audio_file = gr.Audio(type="filepath")
390
+
391
+ with gr.Row().style(equal_height=True):
392
+ text = gr.Textbox(
393
+ label="Transcription",
394
+ placeholder="Transcription Output...",
395
+ lines=10,
396
+ ).style(show_copy_button=True, container=False)
397
+
398
+ with gr.Row().style(equal_height=True):
399
+ summary = gr.Textbox(
400
+ label="Summary", placeholder="Summary Output", lines=5
401
+ )
402
+ keywords = gr.Textbox(
403
+ label="Keywords", placeholder="Keywords Output", lines=5
404
+ )
405
+ label = gr.Label(label="Sentiment Analysis")
406
+ wordcloud_image = gr.Image(label="WordCloud")
407
+
408
+ with gr.Row().style(equal_height=True):
409
+ clear = gr.ClearButton([audio_file,text, summary, keywords, label, wordcloud_image], scale=1, value="Clear 🗑️")
410
+ btn = gr.Button(
411
+ "Get audio insights 🔎", variant="primary", scale=1
412
+ )
413
+ btn.click(
414
+ gio.from_audio_input,
415
+ inputs=[lang, size, audio_file],
416
+ outputs=[text, summary, keywords, label, wordcloud_image],
417
+ )
418
+
419
+
420
+ with block:
421
+ gr.Markdown("### Video Examples")
422
+ gr.Examples(["https://www.youtube.com/shorts/xDNzz8yAH7I","https://www.youtube.com/watch?v=kib6uXQsxBA&pp=ygURc3RldmUgam9icyBzcGVlY2g%3D"], inputs=link)
423
+
424
+ gr.Markdown("### Audio Examples")
425
+ gr.Examples(
426
+ [[os.path.join(os.path.dirname(__file__),"audios/TED_lagrange_point.wav")],[os.path.join(os.path.dirname(__file__),"audios/TED_platon.wav")]],
427
+ inputs=audio_file)
428
 
429
+ gr.Markdown("### About the app:")
430
+
431
+ with gr.Accordion("What is YouTube Insights?", open=False):
432
+ gr.Markdown(
433
+ "YouTube Insights is a tool developed for academic purposes that allows you to analyze YouTube videos or audio files. It provides features like transcription, summarization, keyword extraction, sentiment analysis, and word cloud generation for multimedia content."
434
+ )
435
+
436
+ with gr.Accordion("How does YouTube Insights work?", open=False):
437
+ gr.Markdown(
438
+ "YouTube Insights leverages several powerful AI models and libraries. It uses OpenAI's Whisper for Automatic Speech Recognition (ASR) to transcribe audio content. It summarizes the transcribed text using Facebook's BART model, extracts keywords with VoiceLabT5, performs sentiment analysis with DistilBERT, and generates word clouds."
439
+ )
440
 
441
+ with gr.Accordion("What languages are supported for the analysis?", open=False):
442
+ gr.Markdown(
443
+ "YouTube Insights supports multiple languages for transcription and analysis. You can select your preferred language from the available options when using the app."
444
+ )
445
 
446
+ with gr.Accordion("Can I analyze audio files instead of YouTube videos?", open=False):
447
+ gr.Markdown(
448
+ "Yes, you can analyze audio files directly. Simply upload your audio file to the app, and it will provide the same transcription, summarization, keyword extraction, sentiment analysis, and word cloud generation features."
449
+ )
450
+
451
+ with gr.Accordion("What are the different model sizes available for transcription?", open=False):
452
+ gr.Markdown(
453
+ "The app uses a Speech-to-text model that has different training sizes, from tiny to large. Hence, the bigger the model the accurate the transcription."
454
+ )
455
+
456
+ with gr.Accordion("How long does it take to analyze a video or audio file?", open=False):
457
+ gr.Markdown(
458
+ "The time taken for analysis may vary based on the duration of the video or audio file and the selected model size. Shorter content will be processed more quickly."
459
+ )
460
+
461
+ with gr.Accordion("Who developed YouTube Insights?" ,open=False):
462
+ gr.Markdown(
463
+ "YouTube Insights was developed by students as part of the 2022/23 Master's in Big Data & Data Science program at Universidad Complutense de Madrid for academic purposes (Trabajo de Fin de Master)."
464
+ )
465
 
466
+ gr.HTML(
467
+ """
468
+ <div style="text-align: center; max-width: 500px; margin: 0 auto;">
469
+ <p style="margin-bottom: 10px; font-size: 96%">
470
+ Trabajo de Fin de Máster - Grupo 3
471
+ </p>
472
+ <p style="margin-bottom: 10px; font-size: 90%">
473
+ 2023 Master in Big Data & Data Science - Universidad Complutense de Madrid
474
+ </p>
475
+ </div>
476
+ """
477
+ )
478
 
479
+ demo.launch()
480