DavidNgoue/ComputerVision
0
1import streamlit as st2import cv23import numpy as np4from PIL import Image5from io import BytesIO6import barcode7from barcode.writer import ImageWriter8import qrcode9import tempfile10 11def image_to_bytes(img):12 pil_image = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))13 buffer = BytesIO()14 pil_image.save(buffer, format="PNG")15 return buffer.getvalue()16 17def generate_barcode(link):18 code128 = barcode.get_barcode_class('code128')19 barcode_image = code128(link, writer=ImageWriter())20 buffer = BytesIO()21 barcode_image.write(buffer)22 return Image.open(buffer)23 24def generate_qrcode(link):25 qr = qrcode.QRCode(26 version=2, 27 error_correction=qrcode.constants.ERROR_CORRECT_L, 28 box_size=4, 29 border=2, 30 )31 qr.add_data(link)32 qr.make(fit=True)33 qr_image = qr.make_image(fill_color="black", back_color="white")34 35 small_qr_image = qr_image.resize((512, 512), Image.Resampling.LANCZOS)36 37 buffer = BytesIO()38 small_qr_image.save(buffer, format="PNG")39 return Image.open(buffer)40 41def add_custom_css():42 css = """43 <style>44 body {45 background: linear-gradient(135deg, #a8dadc, #f1faee);46 color: #1d3557;47 font-family: 'Arial', sans-serif;48 animation: backgroundAnimation 10s infinite alternate;49 }50 51 @keyframes backgroundAnimation {52 0% {53 background: linear-gradient(135deg, #a8dadc, #f1faee);54 }55 100% {56 background: linear-gradient(135deg, #f1faee, #457b9d);57 }58 }59 60 .stButton>button {61 background-color: #457b9d;62 color: white;63 border-radius: 5px;64 transition: transform 0.3s, background-color 0.3s;65 box-shadow: 2px 2px 6px rgba(0,0,0,0.2);66 }67 68 .stButton>button:hover {69 transform: scale(1.1);70 background-color: #1d3557;71 }72 73 .stSidebar {74 background: linear-gradient(to bottom, #87CEEB, #FFFFFF);75 color: white;76 font-size: 16px;77 }78 79 .stImage {80 animation: fadeIn 2s ease-in-out;81 }82 83 @keyframes fadeIn {84 0% {85 opacity: 0;86 }87 100% {88 opacity: 1;89 }90 }91 92 header, footer {93 background: #457b9d;94 color: white;95 }96 97 .stMarkdown {98 animation: slideIn 1s ease-out;99 }100 101 @keyframes slideIn {102 0% {103 transform: translateY(-20px);104 opacity: 0;105 }106 100% {107 transform: translateY(0);108 opacity: 1;109 }110 }111 </style>112 """113 st.markdown(css, unsafe_allow_html=True)114 115def add_custom_js():116 js = """117 <script>118 document.addEventListener('DOMContentLoaded', function() {119 const elements = document.querySelectorAll('.stButton>button');120 elements.forEach(button => {121 button.addEventListener('click', () => {122 button.style.backgroundColor = '#a8dadc';123 button.style.transform = 'rotate(360deg)';124 setTimeout(() => button.style.transform = 'rotate(0deg)', 300);125 });126 });127 });128 </script>129 """130 st.markdown(js, unsafe_allow_html=True)131 132def main():133 st.set_page_config(page_title="ADS VISOR - Un autre regard", layout="wide")134 135 add_custom_css()136 add_custom_js()137 138 139 logo_path = "logo.jpg" 140 logo = Image.open(logo_path)141 st.image(logo, width=150, caption="ADS VISOR")142 143 st.title("ADS VISOR - Un autre regard")144 145 st.sidebar.header("Chargement de l'image")146 if "default_image" not in st.session_state:147 st.session_state["default_image"] = None148 149 uploaded_file = st.sidebar.file_uploader("Charge une image", type=["png", "jpg", "jpeg"])150 if uploaded_file is not None:151 image = Image.open(uploaded_file)152 st.session_state["default_image"] = np.array(image)153 st.sidebar.image(image, caption="Image par défaut", use_container_width=True)154 155 if st.session_state["default_image"] is None:156 st.sidebar.warning("Veuillez charger une image pour commencer.")157 return158 159 st.sidebar.header("Fonctionnalités")160 menu_option = st.sidebar.selectbox(161 "Choisissez une fonctionnalité",162 ["Accueil", "Niveaux de couleurs", "Cropping", "Rotation", "Floutage", "Contours", "Génération de Code-barres et QR Code", "Détection Faciale", "À propos de nous"],163 format_func=lambda x: {164 "Accueil": "🏠 Accueil",165 "Niveaux de couleurs": "🖼️ Niveaux de couleurs",166 "Cropping": "✂️ Cropping",167 "Rotation": "🔄 Rotation",168 "Floutage": "🌫️ Floutage",169 "Contours": "🔍 Contours",170 "Génération de Code-barres et QR Code": "📇 Codes numériques",171 "Détection Faciale": "🙂 Détection Faciale",172 "À propos de nous": "👨💻 À propos de nous"173 }.get(x, x)174 )175 176 image_np = st.session_state["default_image"]177 178 if menu_option == "Accueil":179 st.header("Bienvenue sur ADS VISOR")180 st.markdown(181 """182 <div class="stMarkdown">183 <h2>ADS VISOR est une application innovante pour analyser, transformer et explorer vos images. 🖼️✨</h2>184 <p>Que vous soyez un professionnel ou un passionné, découvrez un large éventail de fonctionnalités interactives !</p>185 <ul>186 <li><b>Transformations d'image :</b> Couleurs, niveaux de gris, etc.</li>187 <li><b>Découpage & Rotation :</b> Ajustez vos images à la perfection.</li>188 <li><b>Détection Faciale :</b> Identifiez les visages automatiquement.</li>189 <li><b>Codes-barres & QR Codes :</b> Génération rapide pour vos projets.</li>190 </ul>191 </div>192 """,193 unsafe_allow_html=True194 )195 196 # Niveaux de couleurs197 elif menu_option == "Niveaux de couleurs":198 st.subheader("Niveaux de couleurs")199 200 # Instructions générales sur les niveaux de couleurs201 st.markdown("""202 ### Légende : Fonctionnement des niveaux de couleurs203 - **Les niveaux de couleurs** permettent de manipuler les différentes composantes d'une image : **Rouge**, **Vert**, **Bleu**.204 - L'édition des canaux colorimétriques permet de visualiser une image en mettant en valeur une couleur spécifique.205 206 ### Options disponibles :207 - **Niveaux de Gris** : Convertit l'image en une seule nuance de gris, supprimant les informations de couleur.208 - **Rouge** : Filtre l'image pour ne conserver que la composante rouge.209 - **Vert** : Filtre l'image pour ne conserver que la composante verte.210 - **Jaune** : Combine les composantes rouge et verte pour donner une teinte jaune.211 212 ### Exemple :213 - Si vous choisissez l'option **Rouge**, vous obtiendrez une image où seules les nuances rouges sont visibles, les autres couleurs étant supprimées.214 """)215 216 # Création des onglets pour afficher les images dans différentes couleurs217 tab1, tab2, tab3, tab4 = st.tabs([218 "Gris 🖤", "Rouge ❤️", "Vert 💚", "Jaune 💛"219 ])220 221 # Onglet 1 : Gris222 with tab1:223 gray_image = cv2.cvtColor(image_np, cv2.COLOR_BGR2GRAY)224 st.image(gray_image, caption="Image en Niveaux de Gris", use_container_width=True)225 st.download_button(226 label="Télécharger l'image en gris",227 data=image_to_bytes(gray_image),228 file_name="image_gris.png",229 mime="image/png"230 )231 232 # Onglet 2 : Rouge233 with tab2:234 red = image_np.copy()235 red[:, :, 1:] = 0 # On met les canaux vert et bleu à zéro236 st.image(red, caption="Image Rouge", use_container_width=True)237 st.download_button(238 label="Télécharger l'image rouge",239 data=image_to_bytes(red),240 file_name="image_rouge.png",241 mime="image/png"242 )243 244 # Onglet 3 : Vert245 with tab3:246 green = image_np.copy()247 green[:, :, [0, 2]] = 0 # On met les canaux rouge et bleu à zéro248 st.image(green, caption="Image Verte", use_container_width=True)249 st.download_button(250 label="Télécharger l'image verte",251 data=image_to_bytes(green),252 file_name="image_verte.png",253 mime="image/png"254 )255 256 # Onglet 4 : Jaune257 with tab4:258 yellow = image_np.copy()259 yellow[:, :, 0] = 0 # On supprime la composante bleue260 st.image(yellow, caption="Image Jaune", use_container_width=True)261 st.download_button(262 label="Télécharger l'image jaune",263 data=image_to_bytes(yellow),264 file_name="image_jaune.png",265 mime="image/png"266 )267 268 elif menu_option == "Cropping":269 st.subheader("Cropping - Recadrage d'Image")270 271 # Explications pour les paramètres272 st.markdown(273 """274 ### Légende : Fonctionnement du recadrage275 Le recadrage d'une image consiste à extraire une portion rectangulaire de celle-ci. Pour cela, nous définissons les coordonnées des coins du rectangle :276 277 - **`x1` (Coordonnée x du coin supérieur gauche)** : Position horizontale du début du rectangle (0 ≤ `x1` < largeur-1).278 - **`y1` (Coordonnée y du coin supérieur gauche)** : Position verticale du début du rectangle (0 ≤ `y1` < hauteur-1).279 - **`x2` (Coordonnée x du coin inférieur droit)** : Position horizontale de la fin du rectangle (1 ≤ `x2` ≤ largeur).280 - **`y2` (Coordonnée y du coin inférieur droit)** : Position verticale de la fin du rectangle (1 ≤ `y2` ≤ hauteur).281 282 ### Exemple illustratif :283 Imaginons que vous avez une image de 300x300 pixels. Si vous entrez `x1=50`, `y1=50`, `x2=200`, et `y2=200`, l'image sera recadrée en un rectangle de taille 150x150 pixels, commençant à partir du pixel (50, 50) jusqu'à (200, 200).284 285 ### Étapes :286 1. Saisissez les coordonnées du rectangle que vous souhaitez extraire.287 2. Vous verrez une prévisualisation de la zone sélectionnée avant de la confirmer.288 3. Téléchargez l'image recadrée.289 """290 )291 292 # Entrée des coordonnées293 try:294 x1 = st.number_input("x1 (Coordonnée x du coin supérieur gauche)", 0, image_np.shape[1] - 1, step=1)295 y1 = st.number_input("y1 (Coordonnée y du coin supérieur gauche)", 0, image_np.shape[0] - 1, step=1)296 x2 = st.number_input("x2 (Coordonnée x du coin inférieur droit)", 1, image_np.shape[1], step=1)297 y2 = st.number_input("y2 (Coordonnée y du coin inférieur droit)", 1, image_np.shape[0], step=1)298 299 # Validation des coordonnées300 if x1 >= x2:301 st.error("`x2` doit être strictement supérieur à `x1`. Veuillez corriger vos entrées.")302 elif y1 >= y2:303 st.error("`y2` doit être strictement supérieur à `y1`. Veuillez corriger vos entrées.")304 else:305 # Calcul des dimensions du recadrage306 cropped_width = x2 - x1307 cropped_height = y2 - y1308 309 # Bouton pour générer l'image recadrée310 if st.button("Générer le Recadrage"):311 # Recadrage de l'image312 cropped = image_np[int(y1):int(y2), int(x1):int(x2)]313 314 # Affichage de l'image recadrée315 st.image(cropped, caption="Image Croppée", use_container_width=True)316 317 # Affichage des informations sur la taille du rectangle318 st.write(f"Le rectangle recadré mesure {cropped_width} pixels de large et {cropped_height} pixels de haut.")319 320 # Bouton de téléchargement pour l'image recadrée321 st.download_button(322 label="⬇️ Télécharger l'image croppée",323 data=image_to_bytes(cropped),324 file_name="image_cropped.png",325 mime="image/png"326 )327 328 except Exception as e:329 st.error(f"Une erreur est survenue : {e}")330 331 # Rotation332 elif menu_option == "Rotation":333 st.subheader("Rotation")334 335 # Instructions sur la rotation336 st.markdown("""337 ### Légende : Fonctionnement de la rotation338 - La **rotation** d'une image consiste à tourner l'image autour de son centre selon un angle défini.339 - Vous pouvez choisir un angle prédéfini pour faire pivoter l'image à gauche ou à droite.340 341 ### Options disponibles :342 - Choisissez un angle parmi les options proposées : **45°, 90° ou 180°**.343 - L'image sera ensuite tournée dans le sens des aiguilles d'une montre.344 345 ### Exemple :346 - Si vous choisissez un angle de **90°**, l'image sera pivotée de 90 degrés dans le sens des aiguilles d'une montre par rapport à son centre.347 """)348 349 # Sélection de l'angle de rotation350 angle = st.selectbox("Angle de rotation", [45, 90, 180])351 352 # Calcul des dimensions et application de la rotation353 rows, cols, _ = image_np.shape354 rotation_matrix = cv2.getRotationMatrix2D((cols / 2, rows / 2), angle, 1)355 rotated = cv2.warpAffine(image_np, rotation_matrix, (cols, rows))356 357 # Affichage de l'image après rotation358 st.image(rotated, caption=f"Image Rotée de {angle} degrés", use_container_width=True)359 360 # Bouton de téléchargement pour l'image rotée361 st.download_button(362 label="Télécharger l'image rotée",363 data=image_to_bytes(rotated),364 file_name=f"image_rotated_{angle}.png",365 mime="image/png"366 )367 368 # Floutage369 elif menu_option == "Floutage":370 st.subheader("Floutage")371 372 # Instructions sur le floutage373 st.markdown("""374 ### Légende : Fonctionnement du floutage375 - Le **floutage** est utilisé pour adoucir une image en réduisant les détails fins, ce qui est utile pour l'anonymisation ou pour appliquer des effets esthétiques.376 - Vous pouvez ajuster l'intensité du flou en fonction du niveau choisi.377 378 ### Options disponibles :379 - Le **Niveau de flou (k)** détermine l'intensité du flou appliqué. Plus la valeur est élevée, plus l'image sera floutée.380 - Les valeurs sont comprises entre 1 (très léger flou) et 51 (flou maximal), et doivent être des nombres impairs.381 382 ### Exemple :383 - Si vous choisissez **k = 15**, l'image sera floutée de manière modérée, et vous pourrez voir l'effet de réduction de détails.384 """)385 386 # Sélection du niveau de flou387 blur_level = st.slider("Niveau de flou (k)", min_value=1, max_value=51, step=2, value=15)388 389 # Application du flou sur l'image390 blurred = cv2.GaussianBlur(image_np, (blur_level, blur_level), 0)391 392 # Affichage de l'image floutée393 st.image(blurred, caption=f"Image Floutée (k={blur_level})", use_container_width=True)394 395 # Bouton de téléchargement pour l'image floutée396 st.download_button(397 label="Télécharger l'image floutée",398 data=image_to_bytes(blurred),399 file_name=f"image_blurred_k{blur_level}.png",400 mime="image/png"401 )402 403 # Détection des contours404 elif menu_option == "Contours":405 st.subheader("Contours")406 407 # Instructions sur la détection des contours408 st.markdown("""409 ### Légende : Fonctionnement de la détection de contours410 - La **détection de contours** consiste à identifier les bords distincts de l'image, ce qui permet de mieux comprendre les structures présentes.411 - Elle est souvent utilisée dans des traitements d'image avancés comme la reconnaissance d'objets ou l'analyse d'images.412 413 ### Options disponibles :414 - L'image sera convertie en niveaux de gris, et ensuite un algorithme de détection de contours (Canny) sera appliqué pour extraire les bords de l'image.415 - Cela permet de visualiser les contours dans des images complexes.416 417 ### Exemple :418 - Après l'application de l'algorithme Canny, l'image ne contient que les contours des objets, ce qui peut être utilisé pour détecter des formes, des objets ou d'autres détails visuels importants.419 """)420 421 # Conversion de l'image en niveaux de gris pour la détection des contours422 gray = cv2.cvtColor(image_np, cv2.COLOR_BGR2GRAY)423 edges = cv2.Canny(gray, 100, 200)424 425 # Affichage de l'image avec les contours détectés426 st.image(edges, caption="Contours de l'Image", use_container_width=True)427 428 # Bouton de téléchargement pour l'image avec contours429 st.download_button(430 label="Télécharger l'image avec contours",431 data=image_to_bytes(edges),432 file_name=f"image_edges.png",433 mime="image/png"434 )435 436 437 elif menu_option == "Génération de Code-barres et QR Code":438 st.subheader("Génération de Code-barres et QR Code")439 440 # Légende pour guider l'utilisateur441 st.markdown(442 """443 🔄 **Instructions :**444 1. Saisis un lien ou un texte dans le champ ci-dessous. 445 2. Accède à l'onglet souhaité pour générer un **Code-barres** 📊 ou un **QR Code** 📱. 446 3. Clique sur le bouton **"Générer"** pour voir le résultat et utilise le bouton **⬇️ Télécharger** pour sauvegarder l'image.447 """448 )449 450 # Gestion de l'entrée utilisateur avec sauvegarde dans session_state451 if "link" not in st.session_state:452 st.session_state["link"] = ""453 454 # Entrée utilisateur455 link = st.text_input("🔗 Entre un lien ou un texte pour générer les codes", value=st.session_state["link"])456 if link:457 st.session_state["link"] = link458 459 # Création des onglets460 tab1, tab2 = st.tabs(["📊 Code-barres", "📱 QR Code"])461 462 # Onglet Code-barres463 with tab1:464 st.header("📊 Génération de Code-barres")465 if st.button("Générer le Code-barres"):466 if st.session_state["link"]:467 try:468 # Génération du code-barres469 barcode_image = generate_barcode(st.session_state["link"])470 st.image(barcode_image, caption="Code-barres généré", use_container_width=True)471 472 # Bouton de téléchargement avec icône473 barcode_buffer = BytesIO()474 barcode_image.save(barcode_buffer, format="PNG")475 barcode_buffer.seek(0)476 st.download_button(477 label="⬇️ Télécharger le Code-barres",478 data=barcode_buffer,479 file_name="barcode.png",480 mime="image/png"481 )482 except Exception as e:483 st.error(f"Une erreur est survenue lors de la génération : {e}")484 else:485 st.warning("⚠️ Veuillez entrer un lien ou un texte valide.")486 487 # Onglet QR Code488 with tab2:489 st.header("📱 Génération de QR Code")490 if st.button("Générer le QR Code"):491 if st.session_state["link"]:492 try:493 # Génération du QR Code494 qrcode_image = generate_qrcode(st.session_state["link"])495 st.image(qrcode_image, caption="QR Code généré", use_container_width=True)496 497 # Bouton de téléchargement avec icône498 qrcode_buffer = BytesIO()499 qrcode_image.save(qrcode_buffer, format="PNG")500 qrcode_buffer.seek(0)501 st.download_button(502 label="⬇️ Télécharger le QR Code",503 data=qrcode_buffer,504 file_name="qrcode.png",505 mime="image/png"506 )507 except Exception as e:508 st.error(f"Une erreur est survenue lors de la génération : {e}")509 else:510 st.warning("⚠️ Veuillez entrer un lien ou un texte valide.")511 512 513 elif menu_option == "Détection Faciale":514 st.subheader("Détection Faciale")515 516 # Sélectionner la source de l'image517 source_option = st.radio("Choisissez la source", ("Image Importée", "Autre Image", "Webcam", "Vidéo"))518 519 # Charger le classificateur de visages pré-entrainé de OpenCV520 face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml")521 522 if source_option == "Image Importée":523 if st.session_state["default_image"] is not None:524 img = image_np525 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)526 faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))527 528 # Dessiner des rectangles autour des visages529 for (x, y, w, h) in faces:530 cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2)531 532 st.image(img, caption="Image avec Visages Détectés", use_container_width=True)533 534 elif source_option == "Autre Image":535 uploaded_file_2 = st.file_uploader("Charge une autre image", type=["png", "jpg", "jpeg"])536 if uploaded_file_2 is not None:537 image_2 = Image.open(uploaded_file_2)538 img = np.array(image_2)539 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)540 faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))541 542 # Dessiner des rectangles autour des visages543 for (x, y, w, h) in faces:544 cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2)545 546 st.image(img, caption="Autre Image avec Visages Détectés", use_container_width=True)547 548 elif source_option == "Webcam":549 stframe = st.empty()550 cap = cv2.VideoCapture(0)551 552 while True:553 ret, frame = cap.read()554 if not ret:555 st.write("Erreur dans la lecture de la webcam.")556 break557 558 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)559 faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))560 561 # Dessiner des rectangles autour des visages562 for (x, y, w, h) in faces:563 cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 0, 0), 2)564 565 # Affichage dans le streamlit566 stframe.image(frame, channels="BGR", use_container_width=True)567 568 if cv2.waitKey(1) & 0xFF == ord("q"): # Quitter avec la touche 'q'569 break570 571 cap.release()572 573 elif source_option == "Vidéo":574 video_file = st.file_uploader("Charge une vidéo", type=["mp4", "avi", "mov"])575 if video_file is not None:576 video_bytes = video_file.read()577 with tempfile.NamedTemporaryFile(delete=False) as tmp_file:578 tmp_file.write(video_bytes)579 video_path = tmp_file.name580 581 cap = cv2.VideoCapture(video_path)582 while cap.isOpened():583 ret, frame = cap.read()584 if not ret:585 break586 587 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)588 faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))589 590 # Dessiner des rectangles autour des visages591 for (x, y, w, h) in faces:592 cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 0, 0), 2)593 594 st.image(frame, caption="Vidéo avec Visages Détectés", use_container_width=True)595 596 cap.release()597 598 elif menu_option == "À propos de nous":599 st.header("À propos de nous")600 st.markdown(601 """602 <div style="text-align:center; font-family: Arial; margin: 20px 0;">603 <h2>Notre équipe</h2>604 <p>Nous sommes une équipe passionnée travaillant sur des solutions innovantes.</p>605 </div>606 <div style="display: flex; justify-content: center; gap: 30px; flex-wrap: wrap;">607 <div style="text-align: center;">608 <img src="https://avatars.githubusercontent.com/u/1234567" alt="Ngoue David" style="width: 150px; height: 150px; border-radius: 50%;">609 <h4>Ngoue David</h4>610 <p>🎓 Master 2 IA & Big Data</p>611 <p>📧 <a href="mailto:ngouedavidrogeryannick@gmail.com">ngouedavidrogeryannick@gmail.com</a></p>612 <p>🌐 <a href="https://github.com/TheBeyonder237" target="_blank">Profil GitHub</a></p>613 </div>614 <div style="text-align: center;">615 <img src="https://avatars.githubusercontent.com/u/7654321" alt="Bidzanga Armel" style="width: 150px; height: 150px; border-radius: 50%;">616 <h4>Bidzanga Armel</h4>617 <p>🎓 Master 2 IA & Big Data</p>618 <p>📧 <a href="mailto:armelmbia08@gmail.com">armelmbia08@gmail.com</a></p>619 <p>🌐 <a href="https://github.com/armelmbia10" target="_blank">Profil GitHub</a></p>620 </div>621 <div style="text-align: center;">622 <img src="https://avatars.githubusercontent.com/u/9876543" alt="Nziou Serena" style="width: 150px; height: 150px; border-radius: 50%;">623 <h4>Nziou Serena</h4>624 <p>🎓 Master 2 Administration Systèmes</p>625 <p>📧 <a href="mailto:serenakeliane@gmail.com">serenakeliane@gmail.com</a></p>626 <p>🌐 <a href="https://github.com/serkeli0011" target="_blank">Profil GitHub</a></p>627 </div>628 </div>629 """,630 unsafe_allow_html=True631 )632 633 634 635if __name__ == "__main__":636 main()637 