VerificaUD / app.py
NILC-ICMC-USP's picture
Update app.py
30052d1 verified
import streamlit as st
import tempfile
from pathlib import Path
import base64
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), "./src"))
from verificaUD import verificaUD
print("Verifica-UD running")
translations = {
'🇧🇷': {
'title': 'Verifica-UD',
'subtitle': 'Um verificador de textos de Português Brasileiro anotados segundo as diretrizes das Universal Dependencies',
'input_label': 'Escolha um arquivo .conllu de até 200MB para iniciar a verificação',
'buscando': 'Buscando',
'intro': 'O Verifica-UD faz uma verificação estrutural, léxica e sintática da anotação de textos do Português brasileiro, seguindo o modelo das <a href="https://universaldependencies.org" target="_blank">Universal Dependencies</a>. O Verifica-UD foi desenvolvido dentro do projeto <a href="https://sites.google.com/icmc.usp.br/poetisa" target="_blank">POeTiSA</a>.',
'to_cite': 'Clique aqui para citar o Verifica-UD',
'not_conllu': 'Este arquivo não está no formato correto.',
'no_file': '... à espera de um arquivo .conllu',
'file_uploaded': 'Arquivo recebido corretamente',
'download': 'Baixe o relatório completo.',
'file_name': 'Nome do arquivo',
'sentences': 'Número total de sentenças',
'err': 'erros',
'wrn': 'avisos',
'errFound': 'Erros encontrados',
'wrnFound': 'Avisos emitidos sobre possíveis erros',
'structural': 'estrutural(is)',
'lexical': 'lexical(is)',
'sintatical': 'sintático(s)',
'sentencesOk': 'Número de sentenças sem erros',
'sentencesClean': 'Número de sentenças sem erros ou avisos',
'struct_err': 'Erros estruturais',
'struct_wrn': 'Avisos estruturais',
'tagger_err': 'Erros léxicais',
'tagger_wrn': 'Avisos lexicais',
'parser_err': 'Erros sintáticos',
'parser_wrn': 'Avisos sintáticos',
'options': 'Escolha o tipo de verificação',
'struct': 'Apenas verificação estrutural',
'tagger': 'Verificação estrutural e lexical',
'parser': 'Verificação estrutural, lexical e sintática',
'result': 'Sumário de resultados',
'list_er_wr': 'Mensagens de erros e avisos em ordem de ID da sentença no arquivo',
'congrats': 'Nenhum erro ou aviso encontrado!',
'dev': 'Desenvolvido por'
},
'🇺🇸': {
'title': 'Verifica-UD',
'subtitle': 'A verifier for Universal Dependencies annotation in Brazilian Portuguese',
'input_label': 'Entre um arquivo .conllu para verificar:',
'buscando': 'Buscando',
'intro': 'Verifica-UD faz uma verificação estrutural, léxica e morfosintática para anotação de textos em Português brasileiro, seguindo o modelo internacional do <a href="https://universaldependencies.org" target="_blank">Universal Dependencies</a>. Verifica-UD faz parte do projeto <a href="https://sites.google.com/icmc.usp.br/poetisa" target="_blank">POeTiSA</a>.',
'to_cite': 'Para citar o Verifica-UD',
'not_conllu': 'Este arquivo não está no formato correto.',
'no_file': 'Entre o nome de um arquivo .conllu',
'file_uploaded': 'File uploaded successfully',
'download': 'Baixe o relatório completo.',
'file_name': 'Nome do arquivo',
'sentences': 'Número total de sentenças',
'err': 'erros',
'wrn': 'avisos',
'errFound': 'Erros encontrados:',
'wrnFound': 'Avisos emitidos sobre possíveis erros:',
'structural': 'estruturais',
'lexical': 'lexicais',
'sintatical': 'sintáticos/morfosintáticos',
'sentencesOk': 'Número de sentenças sem erros',
'sentencesClean': 'Número de sentenças sem erros ou avisos',
'struct_err': 'Erros estruturais',
'struct_wrn': 'Avisos estruturais',
'tagger_err': 'Erros léxicais',
'tagger_wrn': 'Avisos lexicais',
'parser_err': 'Erros sintáticos',
'parser_wrn': 'Avisos sintáticos',
'options': 'Escolha o tipo de verificação',
'struct': 'Apenas verificação estrutural',
'tagger': 'Verificação estrutural e lexical',
'parser': 'Verificação estrutural, lexical e sintática',
'result': 'Sumário de resultados',
'list_er_wr': 'Erros e avisos encontrados',
'congrats': 'Nenhum erro ou aviso encontrado!',
'dev': 'Developed by'
},
'🇫🇷': {
'title': 'Verifica-UD',
'subtitle': 'Um verificador de arquivos .conllu para textos anotados em Português Brasileiro',
'input_label': 'Entre um arquivo .conllu para verificar:',
'buscando': 'Buscando',
'intro': 'Verifica-UD faz uma verificação estrutural, léxica e morfosintática para anotação de textos em Português brasileiro, seguindo o modelo internacional do <a href="https://universaldependencies.org" target="_blank">Universal Dependencies</a>. Verifica-UD faz parte do projeto <a href="https://sites.google.com/icmc.usp.br/poetisa" target="_blank">POeTiSA</a>.',
'to_cite': 'Para citar o Verifica-UD',
'not_conllu': 'Este arquivo não está no formato correto.',
'no_file': 'Entre o nome de um arquivo .conllu',
'file_uploaded': 'Arquivo recebido corretamente',
'download': 'Baixe o relatório completo.',
'file_name': 'Nome do arquivo',
'sentences': 'Número total de sentenças',
'err': 'erros',
'wrn': 'avisos',
'errFound': 'Erros encontrados:',
'wrnFound': 'Avisos emitidos sobre possíveis erros:',
'structural': 'estruturais',
'lexical': 'lexicais',
'sintatical': 'sintáticos/morfosintáticos',
'sentencesOk': 'Número de sentenças sem erros',
'sentencesClean': 'Número de sentenças sem erros ou avisos',
'struct_err': 'Erros estruturais',
'struct_wrn': 'Avisos estruturais',
'tagger_err': 'Erros léxicais',
'tagger_wrn': 'Avisos lexicais',
'parser_err': 'Erros sintáticos',
'parser_wrn': 'Avisos sintáticos',
'options': 'Escolha o tipo de verificação',
'struct': 'Só verificação estrutural',
'tagger': 'Verificação estrutural e lexical',
'parser': 'Verificação estrutural, lexical e sintática',
'result': 'Sumário de resultados',
'list_er_wr': 'Erros e avisos encontrados',
'congrats': 'Nenhum erro ou aviso encontrado!',
'dev': 'Développé par'
},
'🇮🇹': {
'title': 'Verifica-UD',
'subtitle': 'Um verificador de arquivos .conllu para textos anotados em Português Brasileiro',
'input_label': 'Entre um arquivo .conllu para verificar:',
'buscando': 'Buscando',
'intro': 'Verifica-UD faz uma verificação estrutural, léxica e morfosintática para anotação de textos em Português brasileiro, seguindo o modelo internacional do <a href="https://universaldependencies.org" target="_blank">Universal Dependencies</a>. Verifica-UD faz parte do projeto <a href="https://sites.google.com/icmc.usp.br/poetisa" target="_blank">POeTiSA</a>.',
'to_cite': 'Para citar o Verifica-UD',
'not_conllu': 'Este arquivo não está no formato correto.',
'no_file': 'Entre o nome de um arquivo .conllu',
'file_uploaded': 'Arquivo recebido corretamente',
'download': 'Baixe o relatório completo.',
'file_name': 'Nome do arquivo',
'sentences': 'Número total de sentenças',
'err': 'erros',
'wrn': 'avisos',
'errFound': 'Erros encontrados:',
'wrnFound': 'Avisos emitidos sobre possíveis erros:',
'structural': 'estruturais',
'lexical': 'lexicais',
'sintatical': 'sintáticos/morfosintáticos',
'sentencesOk': 'Número de sentenças sem erros',
'sentencesClean': 'Número de sentenças sem erros ou avisos',
'struct_err': 'Erros estruturais',
'struct_wrn': 'Avisos estruturais',
'tagger_err': 'Erros lexicais',
'tagger_wrn': 'Avisos lexicais',
'parser_err': 'Erros sintáticos',
'parser_wrn': 'Avisos sintáticos',
'options': 'Escolha o tipo de verificação',
'struct': 'Apenas verificação estrutural',
'tagger': 'Verificação estrutural e lexical',
'parser': 'Verificação estrutural, lexical e sintática',
'result': 'Sumário de resultados',
'list_er_wr': 'Erros e avisos encontrados',
'congrats': 'Nenhum erro ou aviso encontrado!',
'dev': 'Sviluppato da'
},
'🇪🇸': {
'title': 'Verifica-UD',
'subtitle': 'Um verificador de arquivos .conllu para textos anotados em Português Brasileiro',
'input_label': 'Entre um arquivo .conllu para verificar:',
'buscando': 'Buscando',
'intro': 'Verifica-UD faz uma verificação estrutural, léxica e morfosintática para anotação de textos em Português brasileiro, seguindo o modelo internacional do <a href="https://universaldependencies.org" target="_blank">Universal Dependencies</a>. Verifica-UD faz parte do projeto <a href="https://sites.google.com/icmc.usp.br/poetisa" target="_blank">POeTiSA</a>.',
'to_cite': 'Para citar o Verifica-UD',
'not_conllu': 'Este arquivo não está no formato correto.',
'no_file': 'Entre o nome de um arquivo .conllu',
'file_uploaded': 'Arquivo recebido corretamente',
'download': 'Baixe o relatório completo.',
'file_name': 'Nome do arquivo',
'sentences': 'Número total de sentenças',
'err': 'erros',
'wrn': 'avisos',
'errFound': 'Erros encontrados:',
'wrnFound': 'Avisos emitidos sobre possíveis erros:',
'structural': 'estruturais',
'lexical': 'lexicais',
'sintatical': 'sintáticos/morfosintáticos',
'sentencesOk': 'Número de sentenças sem erros',
'sentencesClean': 'Número de sentenças sem erros ou avisos',
'struct_err': 'Erros estruturais',
'struct_wrn': 'Avisos estruturais',
'tagger_err': 'Erros léxicais',
'tagger_wrn': 'Avisos lexicais',
'parser_err': 'Erros sintáticos',
'parser_wrn': 'Avisos sintáticos',
'options': 'Escolha o tipo de verificação',
'struct': 'Apenas verificação estrutural',
'tagger': 'Verificação estrutural e lexical',
'parser': 'Verificação estrutural, lexical e sintática',
'result': 'Sumário de resultados',
'list_er_wr': 'Erros e avisos encontrados',
'congrats': 'Nenhum erro ou aviso encontrado!',
'dev': 'Desarrollado por'
}
}
def img_to_bytes(img_path):
img_bytes = Path(img_path).read_bytes()
encoded = base64.b64encode(img_bytes).decode()
return encoded
def img_to_html(img_path, img_style='max-width: 100%;'):
img_html = f"<img src='data:image/png;base64,{img_to_bytes(img_path)}' style='{img_style}'>"
return img_html
st.markdown("""
<style>
[data-testid="collapsedControl"]::after {
content: " Interface Seetings";
margin-left: 5px;
}
[data-testid="stDownloadButton"] > button {
background: linear-gradient(135deg, #45A049, #3E8E41) !important;
color: #ffffff !important;
border: none !important;
border-radius: 10px !important;
padding: 10px 18px !important;
font-weight: 700 !important;
box-shadow: 0 6px 18px rgba(69, 160, 73, 0.25) !important;
transition: transform 0.06s ease, box-shadow 0.2s ease !important;
}
[data-testid="stDownloadButton"] > button:hover {
transform: translateY(-1px) !important;
box-shadow: 0 10px 24px rgba(6, 182, 212, 0.35) !important;
}
[data-testid="stDownloadButton"] > button:active {
transform: translateY(0) !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.2) !important;
}
body {
background-color: #ffffff !important;
color: #000000 !important;
}
textarea {
background-color: #f0fff0 !important; /* light green background */
color: #333333 !important; /* dark text */
font-size: 16px !important;
font-family: 'Courier New', monospace !important;
border: 2px solid #45A049 !important; /* green border */
border-radius: 8px !important;
padding: 10px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
}
/* Customize expander header */
[data-testid="stExpander"] > div:first-child {
color: #000000;
background-color: #ffffff;
padding: 0px;
border-radius: 8px;
font-weight: bold;
}
</style>
""", unsafe_allow_html=True)
st.set_page_config(
page_title="Verifica-UD",
layout="centered",
initial_sidebar_state="collapsed"
)
# language sidebar
lang_options = {
"🇧🇷 Português": "🇧🇷",
"🇺🇸 English": "🇺🇸",
"🇫🇷 Français": "🇫🇷",
"🇮🇹 Italiano": "🇮🇹",
"🇪🇸 Español": "🇪🇸"
}
selected = st.sidebar.radio("🌐 Interface", list(lang_options.keys()))
t = translations[lang_options[selected]]
# choices sidebar
verif_options = {
t['parser']: "p",
t['tagger']: "t",
t['struct']: "s"
}
v_selected = st.sidebar.radio(t['options'], list(verif_options.keys()))
t_struct, t_tagger, t_parser = True, True, True
if (v_selected == 's'):
t_struct, t_tagger, t_parser = True, False, False
elif (v_selected == 't'):
t_struct, t_tagger, t_parser = True, True, False
elif (v_selected == 'p'):
t_struct, t_tagger, t_parser = True, True, True
# Streamlit app title
st.markdown(f"<h1 style='text-align:center;'>{t['title']}</h1>", unsafe_allow_html=True)
#st.title(t["title"])
st.markdown("<h6 style='text-align:center; margin-top:-20px;'>"+t["subtitle"]+"</h6><br>", unsafe_allow_html=True)
# introduction
#st.write(t["intro"]+"<br><br>", unsafe_allow_html=True)
logo_html = img_to_html("./img/verificaUD.png", img_style="width:120px; margin-right:20px; margin-top:-10px;")
intro_combined = f"""
<div style="display:flex; align-items:flex-start;">
{logo_html}
<div style="flex: 1; text-align: justify; font-size: 1.1em; line-height: 1.2em;">{t['intro']}</div>
</div><br>
"""
st.markdown(intro_combined, unsafe_allow_html=True)
# --- File Upload ---
uploaded_file = st.file_uploader(t['input_label'], type=None)
# --- Run Button ---
if uploaded_file:
st.success("✅ "+t['file_uploaded'])
# Save to a temporary file
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
tmp_file.write(uploaded_file.read())
tmp_path = tmp_file.name
rep_path = tmp_file.name+".rep.txt"
# --- Call Verifica-UD Function ---
def process_file(file_path, rep_path, t_struct, t_tagger, t_parser):
if (t_parser):
mode = "p"
elif (t_tagger):
mode = "t"
elif (t_struct):
mode = "s"
else:
mode = "p"
verificaUD(file_path, rep_path, mode)
with open(rep_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
lines = content.split("\n")
conlluName = lines[0][9:]
bits = lines[2].split()
sentNumber = bits[1]
toknNumber = bits[3][:-1]
bits = lines[3].split()
sentNoErro = bits[3]
bits = lines[4].split()
sentNoErWr = bits[5]
bits = lines[5].split()
struct_err = bits[2]
struct_wrn = bits[5]
bits = lines[6].split()
tagger_err = bits[2]
tagger_wrn = bits[5]
bits = lines[7].split()
parser_err = bits[2]
parser_wrn = bits[5]
result = {
"conlluName": conlluName,
"sentNumber": sentNumber,
"toknNumber": toknNumber,
"sentNoErro": sentNoErro,
"sentNoErWr": sentNoErWr,
"struct_err": struct_err,
"struct_wrn": struct_wrn,
"tagger_err": tagger_err,
"tagger_wrn": tagger_wrn,
"parser_err": parser_err,
"parser_wrn": parser_wrn,
"list_er_wr": "\n".join(map(str, lines[10:])),
"toggles": [t_struct, t_tagger, t_parser]
}
return result, content.replace(file_path, uploaded_file.name)
output, content = process_file(tmp_path, rep_path, t_struct, t_tagger, t_parser)
# --- Display Output ---
st.subheader(t['result'])
total_err = int(output['struct_err'])+int(output['tagger_err'])+int(output['parser_err'])
total_wrn = int(output['struct_wrn'])+int(output['tagger_wrn'])+int(output['parser_wrn'])
st.write(f"{t['sentences']}: {output['sentNumber']} ({output['toknNumber']} tokens)")
st.write(f"{t['errFound']}: ({output['struct_err']} {t['structural']}, {output['tagger_err']} {t['lexical']}, {output['parser_err']} {t['sintatical']})")
st.write(f"{t['wrnFound']}: ({output['struct_wrn']} {t['structural']}, {output['tagger_wrn']} {t['lexical']}, {output['parser_wrn']} {t['sintatical']})")
# Build the table rows dynamically
rows = []
if t_struct:
rows.extend([
f"<tr><td>{t['struct_err']}</td><td>{output['struct_err']} tokens</td><td>{t['struct_wrn']}</td><td>{output['struct_wrn']} tokens</td></tr>"])
if t_tagger:
rows.extend([
f"<tr><td>{t['tagger_err']}</td><td>{output['tagger_err']} tokens</td><td>{t['tagger_wrn']}</td><td>{output['tagger_wrn']} tokens</td></tr>"])
if t_parser:
rows.extend([
f"<tr><td>{t['parser_err']}</td><td>{output['parser_err']} tokens</td><td>{t['parser_wrn']}</td><td>{output['parser_wrn']} tokens</td></tr>"])
# Combine rows into a full table
table_html = f"""
<table style="width:100%; border-collapse: collapse;">
<thead>
<tr style="background-color:#f2f2f2;">
<th colspan="2"; style="text-align:left; padding: 8px;">{output["sentNoErro"]} ({t["sentencesOk"]})</th><th colspan="2"; style="text-align:left; padding: 8px;">{output["sentNoErWr"]} ({t["sentencesClean"]})</th>
</tr>
</thead>
<tbody>
{''.join(rows)}
</tbody>
</table>
"""
# Display the table
st.markdown(table_html, unsafe_allow_html=True)
if (total_err+total_wrn > 0):
# --- Display errors and warnings text area ---
st.write(t['list_er_wr'])
#rawText = st.text_area(t['list_er_wr']+f'({total_err} {t["err"]} - {total_wrn} {t["wrn"]}):\n', output["list_er_wr"], height=300)
colors = ["#16793C", "#14874E", "#1A49B7", "#504AA4"]
lines = output["list_er_wr"].strip().split("\n")
html_lines = []
for line in lines:
fields = line.split("\t")
styled_fields = []
styled_fields.append(f'<span style="color:{colors[0]}; font-weight:bold;">{fields[0]}</span>')
styled_fields.append(f'<span style="color:{colors[1]};">{fields[1]}</span>')
position = fields[2].index(":")
code_msg = [fields[2][:position], fields[2][position+1:]]
#code_msg = fields[2].split(": ")
styled_fields.append(f'<span style="color:{colors[2]}; font-weight:bold;">{code_msg[0]}:</span>')
styled_fields.append(f'<span style="color:{colors[3]};">{code_msg[1]}</span><br>')
html_lines.append(" ".join(styled_fields))
# Combine into scrollable box
styled_html = f"""
<div style="
background-color: #f9f9f9;
border: 1px solid #ccc;
border-radius: 8px;
padding: 10px;
height: 200px;
overflow-y: scroll;
font-family: monospace;
white-space: pre-wrap;
">
{''.join(html_lines)}
</div>
"""
st.markdown(styled_html, unsafe_allow_html=True)
# --- Download Option ---
#st.download_button(label=t['download'],
# data=content,
# file_name=uploaded_file.name.replace(".conllu",".rep.txt"),
# mime="text/plain")
with st.container():
st.markdown('<div class="custom-download-button">', unsafe_allow_html=True)
st.download_button(
label="📥 "+t['download'],
data=content,
file_name=uploaded_file.name.replace(".conllu",".rep.txt"),
mime="text/plain",
key="download_report")
st.markdown('</div>', unsafe_allow_html=True)
else:
st.write(f"{t['congrats']}")
# Clean up temp file
os.remove(tmp_path)
else:
st.info(t['no_file'])
# To cite expander
st.write("<br>", unsafe_allow_html=True)
with st.expander(t["to_cite"], expanded=False):
st.markdown(
"""
<small>
LOPES, L.; DURAN, M. S.; PARDO, T. A. S. Verifica UD - A verifier for Universal Dependencies
annotation in Portuguese. In: Proc. of the UDFest-BR 2023, 2023.
DOI: https://doi.org/10.5753/stil.2023.25485<br>
<i>Links</i>: <a href='https://sol.sbc.org.br/index.php/stil/article/view/25485' target='_blank'><i>URL</i></a> - <a href='https://doi.org/10.5753/stil.2023.25485' target='_blank'><i>DOI</i></a><br>
<i>BibTeX</i>:
</small>
""",
unsafe_allow_html=True)
st.code("""
@inproceedings{Lopes2023VerificaUD,
author = 'LOPES, L. and DURAN, M. S. and PARDO, T. A. S.',
title = 'Verifica UD - A verifier for Universal Dependencies annotation in Portuguese',
booktitle = 'Proc. of the UDFest-BR 2023',
series = 'UDFest-BR',
year = 2023,
pages = '1-8',
url = 'https://sol.sbc.org.br/index.php/stil/article/view/25485',
}""")
st.write("<br>", unsafe_allow_html=True)
# Footer / Footnote
with st.container():
logorow1 = st.columns([3,4,1,4,1,4,3])
with logorow1[1]:
st.markdown("<a href='https://www.icmc.usp.br/' target='_blank'>"+img_to_html('./img/icmc.png')+"</a>",unsafe_allow_html=True)
with logorow1[3]:
st.markdown("<a href='https://c4ai.inova.usp.br/pt/inicio/' target='_blank'>"+img_to_html('./img/c4ia.png')+"</a>",unsafe_allow_html=True)
with logorow1[5]:
st.markdown("<a href='https://sites.google.com/view/nilc-usp/' target='_blank'>"+img_to_html('./img/nilc-removebg.png','max-width:80%')+"</a>",unsafe_allow_html=True)
logorow2 = st.columns([1,4,1,4,1,5,1,4,1])
with logorow2[1]:
st.markdown("<a href='https://inova.usp.br/' target='_blank'>"+img_to_html('./img/inova_nobackground.png')+"</a>",unsafe_allow_html=True)
with logorow2[3]:
st.markdown("<a href='https://softex.br/' target='_blank'>" + img_to_html('./img/softex_nobackground.png') + "</a>",unsafe_allow_html=True)
with logorow2[5]:
st.markdown("<a href='https://www.gov.br/mcti/pt-br' target='_blank'>" + img_to_html('./img/mcti_nobackground.png') + "</a>",unsafe_allow_html=True)
with logorow2[7]:
st.markdown("<a href='https://www.motorola.com.br/' target='_blank'>"+img_to_html('./img/motorola_nobackground.png', 'max-width:70%; object-position: center bottom')+"</a>",unsafe_allow_html=True)
creditrow = st.columns([3,18,3])
with creditrow[1]:
st.markdown('<p style="text-align: center;margin-top:10px"> '+t["dev"]+' Lucelene Lopes\
<a href="https://github.com/LuceleneL" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-github" viewBox="0 0 16 16">\
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/>\
</svg></a> <br> open source <a href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a></p>',unsafe_allow_html=True)