Spaces:
Sleeping
Sleeping
import os | |
import matplotlib | |
os.environ['MPLCONFIGDIR'] = '/tmp' | |
import streamlit as st | |
import requests | |
import json | |
import pandas as pd | |
import matplotlib.pyplot as plt | |
import time | |
import numpy as np | |
import altair as alt | |
from PIL import Image | |
import plotly.graph_objects as go | |
import plotly.express as px | |
from streamlit_lottie import st_lottie | |
import random | |
from streamlit_lottie import st_lottie # Note: this is duplicated | |
# Configuration de la page | |
st.set_page_config( | |
page_title="API NLU Darija - Mohammed MEDIANI", | |
page_icon="🚀", | |
layout="wide", | |
initial_sidebar_state="expanded" | |
) | |
# Fonctions utilitaires | |
def call_api(text): | |
"""Appelle l'API NLU Darija et retourne le résultat""" | |
api_url = "https://mediani-darija-aicc-api.hf.space/predict" | |
try: | |
start_time = time.time() | |
response = requests.post( | |
api_url, | |
headers={"Content-Type": "application/json"}, | |
data=json.dumps({"text": text}) | |
) | |
response_time = (time.time() - start_time) * 1000 # en ms | |
if response.status_code == 200: | |
result = response.json() | |
return result, response_time | |
else: | |
return None, response_time | |
except Exception as e: | |
st.error(f"Erreur de connexion: {str(e)}") | |
return None, 0 | |
def get_intent_description(intent): | |
"""Retourne la description d'une intention""" | |
descriptions = { | |
"consulter_solde": "L'utilisateur souhaite connaître son solde ou crédit restant sur son compte.", | |
"reclamer_facture": "L'utilisateur signale un problème avec sa facture ou conteste un montant facturé.", | |
"declarer_panne": "L'utilisateur signale un dysfonctionnement technique avec son service ou équipement.", | |
"info_forfait": "L'utilisateur demande des informations sur un forfait existant ou nouveau.", | |
"recuperer_mot_de_passe": "L'utilisateur a besoin d'aide pour récupérer ou réinitialiser son mot de passe.", | |
"salutations": "L'utilisateur salue le service client ou initie une conversation.", | |
"remerciements": "L'utilisateur exprime sa gratitude pour l'aide reçue.", | |
"demander_agent_humain": "L'utilisateur souhaite être mis en relation avec un conseiller humain.", | |
"hors_scope": "La demande ne correspond à aucune intention prédéfinie dans notre système." | |
} | |
return descriptions.get(intent, "Description non disponible") | |
def get_intent_icon(intent): | |
"""Retourne une icône associée à une intention""" | |
icons = { | |
"consulter_solde": "💰", | |
"reclamer_facture": "📄", | |
"declarer_panne": "🔧", | |
"info_forfait": "ℹ️", | |
"recuperer_mot_de_passe": "🔑", | |
"salutations": "👋", | |
"remerciements": "🙏", | |
"demander_agent_humain": "👨💼", | |
"hors_scope": "❓" | |
} | |
return icons.get(intent, "🔍") | |
def get_intent_color(intent): | |
"""Retourne une couleur associée à une intention""" | |
colors = { | |
"consulter_solde": "#1f77b4", | |
"reclamer_facture": "#ff7f0e", | |
"declarer_panne": "#d62728", | |
"info_forfait": "#2ca02c", | |
"recuperer_mot_de_passe": "#9467bd", | |
"salutations": "#8c564b", | |
"remerciements": "#e377c2", | |
"demander_agent_humain": "#7f7f7f", | |
"hors_scope": "#bcbd22" | |
} | |
return colors.get(intent, "#17becf") | |
def load_lottie(url): | |
"""Charge une animation Lottie depuis une URL""" | |
try: | |
r = requests.get(url) | |
if r.status_code != 200: | |
return None | |
return r.json() | |
except: | |
return None | |
# Charger les animations | |
lottie_ai = load_lottie("https://assets8.lottiefiles.com/packages/lf20_ikvz7qhc.json") | |
lottie_process = load_lottie("https://assets6.lottiefiles.com/packages/lf20_khzniaya.json") | |
# Style CSS personnalisé | |
st.markdown(""" | |
<style> | |
/* Style général */ | |
.main-title { | |
font-size: 2.8rem !important; | |
color: #1E3A8A; | |
padding-bottom: 0.5rem; | |
border-bottom: 3px solid #3B82F6; | |
font-weight: 700; | |
text-shadow: 0px 2px 2px rgba(0,0,0,0.1); | |
margin-bottom: 1.5rem; | |
} | |
.sub-title { | |
font-size: 1.8rem !important; | |
color: #1E3A8A; | |
margin-top: 1.5rem; | |
margin-bottom: 1rem; | |
font-weight: 600; | |
} | |
.section-title { | |
font-size: 1.4rem !important; | |
color: #2563EB; | |
margin-top: 1.2rem; | |
margin-bottom: 0.8rem; | |
font-weight: 500; | |
} | |
/* Boîtes d'information */ | |
.info-box { | |
background-color: #F8FAFC; | |
padding: 1.5rem; | |
border-radius: 0.75rem; | |
border: 2px solid #E2E8F0; | |
margin-bottom: 1.5rem; | |
box-shadow: 0 4px 12px rgba(0,0,0,0.08); | |
color: #1A202C; | |
font-size: 16px; | |
line-height: 1.7; | |
} | |
.info-box p { | |
margin-bottom: 12px; | |
font-weight: 500; | |
} | |
.info-box strong { | |
color: #1E3A8A; | |
font-weight: 700; | |
} | |
.info-box ul { | |
margin-left: 20px; | |
color: #4A5568; | |
} | |
.info-box li { | |
margin-bottom: 8px; | |
font-weight: 500; | |
} | |
.warning-box { | |
background-color: #FEF3C7; | |
padding: 1.2rem; | |
border-radius: 0.5rem; | |
border-left: 5px solid #F59E0B; | |
margin-bottom: 1.5rem; | |
box-shadow: 0 4px 6px rgba(0,0,0,0.05); | |
} | |
.success-box { | |
background-color: #ECFDF5; | |
padding: 1.2rem; | |
border-radius: 0.5rem; | |
border-left: 5px solid #10B981; | |
margin-bottom: 1.5rem; | |
box-shadow: 0 4px 6px rgba(0,0,0,0.05); | |
} | |
/* Boutons */ | |
.stButton>button { | |
border-radius: 0.5rem; | |
font-weight: 500; | |
transition: all 0.3s ease; | |
} | |
.stButton>button:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 4px 8px rgba(0,0,0,0.1); | |
} | |
.example-button { | |
margin: 0.3rem; | |
} | |
/* Tags et badges */ | |
.intent-tag { | |
background-color: #1E3A8A; | |
color: white; | |
padding: 0.4rem 1rem; | |
border-radius: 2rem; | |
font-weight: bold; | |
display: inline-block; | |
margin-bottom: 0.8rem; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
} | |
.badge { | |
padding: 0.2rem 0.6rem; | |
border-radius: 2rem; | |
font-size: 0.8rem; | |
font-weight: bold; | |
margin-left: 0.5rem; | |
} | |
/* Conteneurs */ | |
.glass-container { | |
background: rgba(255, 255, 255, 0.7); | |
backdrop-filter: blur(10px); | |
border-radius: 10px; | |
border: 1px solid rgba(255, 255, 255, 0.18); | |
padding: 1.5rem; | |
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); | |
} | |
/* Animations */ | |
@keyframes fadeIn { | |
from { opacity: 0; } | |
to { opacity: 1; } | |
} | |
.fade-in { | |
animation: fadeIn 0.5s ease-in-out; | |
} | |
@keyframes slideInFromLeft { | |
0% { | |
transform: translateX(-30px); | |
opacity: 0; | |
} | |
100% { | |
transform: translateX(0); | |
opacity: 1; | |
} | |
} | |
.slide-in { | |
animation: slideInFromLeft 0.5s ease-out; | |
} | |
/* Mise en page de l'en-tête */ | |
.header-content { | |
display: flex; | |
align-items: center; | |
margin-bottom: 1.5rem; | |
} | |
.description-container { | |
flex: 3; | |
padding-right: 1.5rem; | |
} | |
.image-container { | |
flex: 2; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
/* Responsive design */ | |
@media (max-width: 768px) { | |
.main-title { font-size: 2rem !important; } | |
.sub-title { font-size: 1.5rem !important; } | |
.section-title { font-size: 1.2rem !important; } | |
.header-content { flex-direction: column; } | |
.description-container { padding-right: 0; padding-bottom: 1.5rem; } | |
} | |
/* Table des performances */ | |
.styled-table { | |
width: 100%; | |
border-collapse: collapse; | |
margin: 1.5rem 0; | |
border-radius: 8px; | |
overflow: hidden; | |
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); | |
} | |
.styled-table thead tr { | |
background-color: #1E3A8A; | |
color: white; | |
text-align: left; | |
} | |
.styled-table th, | |
.styled-table td { | |
padding: 12px 15px; | |
} | |
.styled-table tbody tr { | |
border-bottom: 1px solid #dddddd; | |
} | |
.styled-table tbody tr:nth-of-type(even) { | |
background-color: #f9fafb; | |
} | |
.styled-table tbody tr:last-of-type { | |
border-bottom: 2px solid #1E3A8A; | |
} | |
/* Masquer les éléments par défaut de Streamlit qu'on ne veut pas voir */ | |
#MainMenu {visibility: hidden;} | |
footer {visibility: hidden;} | |
.viewerBadge_container__r5tak {display: none;} | |
</style> | |
""", unsafe_allow_html=True) | |
# Configuration des colonnes principales | |
header_col1, header_col2 = st.columns([2, 5]) | |
# Logo et informations de base | |
with header_col1: | |
try: | |
st.image("logo_est_nador.png", width=200) | |
except: | |
st.markdown("<h3>EST Nador</h3>", unsafe_allow_html=True) | |
st.warning("Logo non trouvé. Placez 'logo_est_nador.png' dans le dossier du projet.") | |
st.markdown('<div class="slide-in">', unsafe_allow_html=True) | |
st.markdown("### Stage de Fin d'Études") | |
st.markdown("**Étudiant:** Mohammed MEDIANI") | |
st.markdown("**Filière:** IAID - EST Nador") | |
st.markdown("**Date:** Juin 2025") | |
st.markdown("</div>", unsafe_allow_html=True) | |
st.markdown("---") | |
st.markdown('<div class="fade-in">', unsafe_allow_html=True) | |
st.markdown("### Encadrement") | |
st.markdown("- **Pr. ACHSAS SANAE** (Académique)") | |
st.markdown("- **Mme. Aya BENNANI** (Professionnelle)") | |
st.markdown("</div>", unsafe_allow_html=True) | |
st.markdown("---") | |
st.markdown('<div class="fade-in">', unsafe_allow_html=True) | |
st.markdown("### Informations sur l'API") | |
st.markdown("**URL:** [mediani-darija-aicc-api.hf.space](https://mediani-darija-aicc-api.hf.space)") | |
st.markdown("**Documentation:** [API Docs](https://mediani-darija-aicc-api.hf.space/docs)") | |
st.markdown("</div>", unsafe_allow_html=True) | |
# Titre principal et description | |
with header_col2: | |
st.markdown('<h1 class="main-title">API de NLU pour le Dialecte Marocain (Darija)</h1>', unsafe_allow_html=True) | |
# Description du projet - Style raffiné et concis pour équilibrer avec la grande animation | |
st.markdown(""" | |
<div class="info-box fade-in" style="margin-bottom: 15px; padding: 1.2rem; border: 1px solid #E2E8F0;"> | |
<p style="font-size: 16px; margin-bottom: 10px;">Ce projet vise à concevoir et déployer une <strong>API de compréhension du langage naturel (NLU)</strong> spécialisée pour la Darija marocaine. L'objectif est d'améliorer l'expérience client en permettant aux systèmes automatisés de comprendre les requêtes exprimées dans ce dialecte.</p> | |
<p style="font-size: 16px; margin-bottom: 10px;">L'API identifie 9 intentions différentes et s'intègre avec la plateforme AICC de Huawei pour le traitement des requêtes clients.</p> | |
<p style="font-size: 15px; color: #3B82F6; text-align: center;"><strong>✨ Cette démonstration interactive vous permet d'explorer les capacités de l'API en temps réel</strong></p> | |
</div> | |
""", unsafe_allow_html=True) | |
# Animation Lottie centrée sous le texte - Taille agrandie pour remplir l'espace vertical | |
st.markdown('<div style="display: flex; justify-content: center; align-items: center; margin-bottom: 20px; padding: 10px; background-color: rgba(240, 249, 255, 0.3); border-radius: 15px;">', unsafe_allow_html=True) | |
if lottie_ai: | |
st_lottie(lottie_ai, height=400, width=680, key="ai_animation", quality="high") | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Espace réduit car l'animation est plus grande et remplit déjà bien l'espace | |
st.markdown("<div style='height: 10px;'></div>", unsafe_allow_html=True) | |
# Onglets principaux avec icônes | |
tab1, tab2, tab3 = st.tabs(["🔍 Démonstration", "📊 Performances", "🏗️ Architecture"]) | |
# Onglet Démonstration | |
with tab1: | |
st.markdown('<h2 class="sub-title slide-in">Testez l\'API en direct</h2>', unsafe_allow_html=True) | |
# Description du service | |
st.info(""" | |
**Cette interface vous permet de tester en temps réel notre API de compréhension du langage naturel spécialisée pour la Darija marocaine.** | |
**Instructions:** | |
1. Entrez un texte en Darija ou sélectionnez un exemple prédéfini | |
2. Cliquez sur le bouton "Analyser l'intention" | |
3. Observez les résultats de la détection d'intention | |
L'API est optimisée pour comprendre la Darija dans ses différentes variantes et avec le code-switching (mélange avec le français). | |
""") | |
# Exemples prédéfinis | |
st.markdown('<h3 class="section-title">Exemples à tester</h3>', unsafe_allow_html=True) | |
# Organisation des exemples par catégories | |
with st.expander("🔄 Exemples par catégorie d'intention", expanded=True): | |
tab_expl1, tab_expl2, tab_expl3 = st.tabs(["Requêtes techniques", "Interactions", "Code-switching"]) | |
with tab_expl1: | |
exemples_tech = { | |
"Consulter solde": "بغيت نعرف شحال باقي ليا في رصيدي", | |
"Déclarer panne": "ماكيخدمش عندي لانترنيت هاذي شي سيمانة", | |
"Réclamation facture": "فاكتورة هاد الشهر غالية بزاف، بغيت نشوف علاش", | |
"Info forfait": "شنو هوما لوفر ديال لانترنيت لي كاينين دابا", | |
"Récupérer mot de passe": "نسيت mon mot de passe ديالي واش يمكن تساعدني؟" | |
} | |
cols = st.columns(3) | |
for i, (label, exemple) in enumerate(exemples_tech.items()): | |
with cols[i % 3]: | |
if st.button(f"{label}", key=f"tech_btn_{i}", help=exemple): | |
st.session_state["user_input"] = exemple | |
st.rerun() | |
with tab_expl2: | |
exemples_inter = { | |
"Salutations": "salam 3lik bkhir", | |
"Remerciement": "شكرا بزاف على المساعدة ديالكم، كنتو مزيانين معايا", | |
"Demander agent": "Brit nhdar m3a service client ma bghitch robot", | |
"Hors scope": "واش كاين شي طريقة باش نلعب تينيس فهاد الويكاند؟" | |
} | |
cols = st.columns(2) | |
for i, (label, exemple) in enumerate(exemples_inter.items()): | |
with cols[i % 2]: | |
if st.button(f"{label}", key=f"inter_btn_{i}", help=exemple): | |
st.session_state["user_input"] = exemple | |
st.rerun() | |
with tab_expl3: | |
exemples_code = { | |
"Solde (code-switching)": "بغيت نعرف le solde ديالي شحال باقي", | |
"Panne (code-switching)": "عندي problème فالفاكتورة ديالي", | |
"Mot de passe (code-switching)": "نسيت mon mot de passe ديالي واش يمكن تساعدني؟", | |
"Salutations (code-switching)": "bonjour صاحبي، كيفاش يمكن لي نساعدك؟" | |
} | |
cols = st.columns(2) | |
for i, (label, exemple) in enumerate(exemples_code.items()): | |
with cols[i % 2]: | |
if st.button(f"{label}", key=f"code_btn_{i}", help=exemple): | |
st.session_state["user_input"] = exemple | |
st.rerun() | |
# Zone de texte pour l'entrée utilisateur | |
st.markdown('<h3 class="section-title">Votre requête</h3>', unsafe_allow_html=True) | |
if "user_input" not in st.session_state: | |
st.session_state["user_input"] = "بغيت نعرف شحال باقي ليا في رصيدي" | |
user_input = st.text_area("Entrez un texte en Darija:", | |
value=st.session_state["user_input"], | |
height=100, | |
key="input_area", | |
help="Vous pouvez entrer du texte en Darija pure ou mélangé avec du français") | |
# Bouton d'analyse avec animation de chargement | |
col1, col2, col3 = st.columns([1, 1, 1]) | |
with col2: | |
analyze_btn = st.button("🔍 Analyser l'intention", | |
key="analyze_btn", | |
type="primary", | |
help="Cliquez pour analyser le texte") | |
# Analyser le texte si le bouton est cliqué | |
if analyze_btn: | |
with st.spinner("Analyse en cours..."): | |
# Afficher l'animation de traitement pendant l'appel à l'API | |
if lottie_process: | |
placeholder = st.empty() | |
with placeholder.container(): | |
st_lottie(lottie_process, height=120, key="process_animation") | |
result, response_time = call_api(user_input) | |
# Supprimer l'animation une fois le résultat obtenu | |
if lottie_process: | |
placeholder.empty() | |
if result: | |
# Afficher les résultats dans un cadre | |
st.markdown("---") | |
st.markdown('<h3 class="section-title fade-in">Résultats de l\'analyse</h3>', unsafe_allow_html=True) | |
# Créer un conteneur de style "glass" pour les résultats | |
st.markdown('<div class="glass-container">', unsafe_allow_html=True) | |
# Créer deux colonnes pour les résultats | |
res_col1, res_col2 = st.columns([1, 1]) | |
with res_col1: | |
# Icône et tag d'intention | |
intent_icon = get_intent_icon(result["intent"]) | |
st.markdown(f'<div class="intent-tag" style="background-color: {get_intent_color(result["intent"])};">{intent_icon} {result["intent"]}</div>', unsafe_allow_html=True) | |
# Description de l'intention | |
st.markdown(f"**Description:** {get_intent_description(result['intent'])}") | |
# Temps de réponse avec badge coloré | |
speed_class = "success" if response_time < 200 else "warning" if response_time < 500 else "danger" | |
st.markdown(f""" | |
<div> | |
<span>⏱️ Temps de réponse:</span> | |
<span class="badge" style="background-color: {'#10B981' if speed_class == 'success' else '#F59E0B' if speed_class == 'warning' else '#EF4444'};"> | |
{response_time:.2f} ms | |
</span> | |
</div> | |
""", unsafe_allow_html=True) | |
with res_col2: | |
# Utiliser Plotly pour un graphique interactif de confiance | |
fig = go.Figure() | |
# Ajouter la barre de fond | |
fig.add_trace(go.Bar( | |
x=[1], | |
y=['Confiance'], | |
orientation='h', | |
marker=dict(color='rgba(240, 240, 240, 0.5)'), | |
width=0.5, | |
hoverinfo='skip', | |
showlegend=False | |
)) | |
# Ajouter la barre principale | |
fig.add_trace(go.Bar( | |
x=[result["confidence"]], | |
y=['Confiance'], | |
orientation='h', | |
marker=dict(color=get_intent_color(result["intent"])), | |
width=0.5, | |
hovertemplate=f'Confiance: {result["confidence"]*100:.1f}%<extra></extra>' | |
)) | |
# Configuration de la mise en page | |
fig.update_layout( | |
title=f"Score: {result['confidence']*100:.1f}%", | |
height=150, | |
margin=dict(l=20, r=20, t=40, b=20), | |
xaxis=dict( | |
range=[0, 1], | |
tickvals=[0, 0.25, 0.5, 0.75, 1], | |
ticktext=['0%', '25%', '50%', '75%', '100%'], | |
gridcolor='rgba(0, 0, 0, 0.1)' | |
), | |
barmode='overlay', | |
bargap=0.1, | |
showlegend=False | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
# Fermer le conteneur en verre | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Explication technique | |
with st.expander("🔬 Explication technique", expanded=False): | |
st.markdown(""" | |
Le texte passe par plusieurs étapes de traitement dans notre API: | |
1. **Prétraitement**: | |
- Normalisation du texte arabe (alif, yaa, etc.) | |
- Gestion spéciale des caractères non-arabes | |
- Traitement du code-switching Darija-Français | |
2. **Tokenisation**: | |
- Conversion en tokens avec le tokenizer de MARBERTv2 | |
- Support des tokens spéciaux pour la Darija | |
3. **Inférence**: | |
- Passage dans le modèle fine-tuné sur notre corpus personnalisé | |
- Application d'une couche linéaire de classification | |
4. **Post-traitement**: | |
- Détermination de l'intention la plus probable | |
- Calcul du score de confiance via softmax | |
Le système utilise un modèle de type Transformer spécifiquement optimisé pour la Darija marocaine et ses spécificités dialectales. | |
""") | |
# Afficher le payload JSON avec coloration syntaxique | |
st.markdown("#### Requête et réponse JSON:") | |
col1, col2 = st.columns(2) | |
with col1: | |
st.code(json.dumps({"text": user_input}, indent=2, ensure_ascii=False), language="json") | |
with col2: | |
st.code(json.dumps(result, indent=2, ensure_ascii=False), language="json") | |
# Exemples similaires | |
with st.expander("📚 Exemples similaires", expanded=False): | |
st.markdown(f"### Autres exemples pour l'intention: {result['intent']}") | |
# Dictionnaire d'exemples par intention | |
exemples_par_intention = { | |
"consulter_solde": [ | |
"شحال عندي في لكارط ديالي؟", | |
"بقا ليا شحال في الكريدي؟", | |
"واش ممكن تشوف ليا رصيدي؟", | |
"فين يمكن لي نراقب الكريدي ديالي؟", | |
"بغيت نعرف le solde ديالي شحال باقي" | |
], | |
"reclamer_facture": [ | |
"عندي مشكل في الفاكتورة", | |
"الفاتورة ديال هاد الشهر مضاعفة على الشهر لي فات!", | |
"كينقصوني فلوس بزاف ف الفاكتورة", | |
"مكنستهلكش هاد القدر ديال الميكا، كاين خطأ", | |
"عندي problème فالفاكتورة ديالي" | |
], | |
"declarer_panne": [ | |
"ماكيخدمش عندي لانترنيت هاذي شي سيمانة", | |
"التيليفون ما كيشارجيش، عيطو ليا بسرعة", | |
"عندي بروبليم فلانترنيت ديالي، كيقطع بزاف", | |
"ماكيدوزش عندي لابيل ديال التيليفزيون", | |
"j'ai un problème تقطع عليا الضو ديال مودام الويفي" | |
], | |
"info_forfait": [ | |
"بغيت نبدل الفورفيه ديالي لشي وحدة أحسن", | |
"واش كاين شي فورفي ديال سوشيال ميديا؟", | |
"بغيت نخلص باش نزيد ف لانترنت ديالي", | |
"أشنو هو أحسن فورفيه عندكم؟", | |
"je cherche un forfait مزيان للانترنت" | |
], | |
"recuperer_mot_de_passe": [ | |
"نسيت mon mot de passe ديالي واش يمكن تساعدني؟", | |
"كيفاش نقدر نسترجع كلمة السر؟", | |
"نسيت الكود ديالي ديال الكونيكسيون", | |
"بغيت نبدل لو دو باس ديالي", | |
"j'ai oublié لو دو باس ديال l'application" | |
], | |
"salutations": [ | |
"صباح الخير، كيفاش يمكن لي نتواصل معاكم؟", | |
"السلام عليكم، بغيت نسولكم واحد السؤال", | |
"مرحبا، شكون لي كيهضر؟", | |
"آلو، واش نتا روبوت ولا بنادم حقيقي؟", | |
"bonjour صاحبي، كيفاش يمكن لي نساعدك؟" | |
], | |
"remerciements": [ | |
"شكرا بزاف على المساعدة ديالكم", | |
"باراكا لاهو فيك، راك عاونتيني بزاف", | |
"ميرسي بزاف، ربي يجازيك بخير", | |
"متشكر على الوقت ديالك", | |
"merci بزاف على المساعدة ديالك" | |
], | |
"demander_agent_humain": [ | |
"بغيت نتكلم مع شي واحد حقيقي ماشي روبو", | |
"واش ممكن تعاوني نهضر مع شي كونسيي؟", | |
"بغيت شي واحد يتواصل معايا هاتفيا", | |
"هادشي ماشي هو هداك لي كنبغي، خاصني بنادم نهضر معاه", | |
"je veux parler à un conseiller حقيقي" | |
], | |
"hors_scope": [ | |
"واش كاين شي طريقة باش نلعب تينيس فهاد الويكاند؟", | |
"كيفاش طقس غدا فالرباط؟", | |
"شنو الأفلام الجديدة فالسينما؟", | |
"فين نقدر نلقى دواء بارسيتامول فالحي ديالي؟", | |
"je cherche un restaurant قريب من هنا" | |
] | |
} | |
# Afficher les exemples pour l'intention détectée | |
if result["intent"] in exemples_par_intention: | |
examples = exemples_par_intention[result["intent"]] | |
for ex in examples: | |
st.markdown(f"- `{ex}`") | |
# Bouton pour tester un exemple aléatoire | |
if st.button("🎲 Tester un exemple aléatoire", key="random_example"): | |
st.session_state["user_input"] = random.choice(examples) | |
st.rerun() | |
else: | |
st.write("Pas d'exemples disponibles pour cette intention.") | |
# Onglet Performances | |
with tab2: | |
st.markdown('<h2 class="sub-title slide-in">Performance du modèle</h2>', unsafe_allow_html=True) | |
# Description des performances | |
st.info(""" | |
**Cette section présente les performances du modèle MARBERTv2 fine-tuné sur notre corpus de Darija.** | |
Les métriques ont été calculées sur un ensemble de test représentatif contenant 1 192 exemples issus de conversations réelles. | |
Notre approche est basée sur un modèle de type Transformer pré-entraîné sur l'arabe (MARBERTv2) et spécifiquement adapté aux particularités dialectales de la Darija marocaine. | |
""") | |
# Métriques globales | |
st.markdown('<h3 class="section-title">Métriques globales</h3>', unsafe_allow_html=True) | |
# Créer des colonnes pour les métriques avec des indicateurs visuels | |
metric_cols = st.columns(4) | |
# Créer des graphiques Gauge pour chaque métrique | |
with metric_cols[0]: | |
fig = go.Figure(go.Indicator( | |
mode = "gauge+number", | |
value = 92.8, | |
domain = {'x': [0, 1], 'y': [0, 1]}, | |
title = {'text': "Accuracy", 'font': {'size': 24}}, | |
gauge = { | |
'axis': {'range': [None, 100], 'tickwidth': 1, 'tickcolor': "darkblue"}, | |
'bar': {'color': "#1E3A8A"}, | |
'bgcolor': "white", | |
'borderwidth': 2, | |
'bordercolor': "gray", | |
'steps': [ | |
{'range': [0, 70], 'color': '#FFEDD5'}, | |
{'range': [70, 85], 'color': '#FEF3C7'}, | |
{'range': [85, 100], 'color': '#ECFDF5'}], | |
} | |
)) | |
fig.update_layout(height=200, margin=dict(l=20, r=20, t=50, b=20)) | |
st.plotly_chart(fig, use_container_width=True) | |
with metric_cols[1]: | |
fig = go.Figure(go.Indicator( | |
mode = "gauge+number", | |
value = 93.1, | |
domain = {'x': [0, 1], 'y': [0, 1]}, | |
title = {'text': "Precision", 'font': {'size': 24}}, | |
gauge = { | |
'axis': {'range': [None, 100], 'tickwidth': 1, 'tickcolor': "darkblue"}, | |
'bar': {'color': "#1E40AF"}, | |
'bgcolor': "white", | |
'borderwidth': 2, | |
'bordercolor': "gray", | |
'steps': [ | |
{'range': [0, 70], 'color': '#FFEDD5'}, | |
{'range': [70, 85], 'color': '#FEF3C7'}, | |
{'range': [85, 100], 'color': '#ECFDF5'}], | |
} | |
)) | |
fig.update_layout(height=200, margin=dict(l=20, r=20, t=50, b=20)) | |
st.plotly_chart(fig, use_container_width=True) | |
with metric_cols[2]: | |
fig = go.Figure(go.Indicator( | |
mode = "gauge+number", | |
value = 92.8, | |
domain = {'x': [0, 1], 'y': [0, 1]}, | |
title = {'text': "Recall", 'font': {'size': 24}}, | |
gauge = { | |
'axis': {'range': [None, 100], 'tickwidth': 1, 'tickcolor': "darkblue"}, | |
'bar': {'color': "#2563EB"}, | |
'bgcolor': "white", | |
'borderwidth': 2, | |
'bordercolor': "gray", | |
'steps': [ | |
{'range': [0, 70], 'color': '#FFEDD5'}, | |
{'range': [70, 85], 'color': '#FEF3C7'}, | |
{'range': [85, 100], 'color': '#ECFDF5'}], | |
} | |
)) | |
fig.update_layout(height=200, margin=dict(l=20, r=20, t=50, b=20)) | |
st.plotly_chart(fig, use_container_width=True) | |
with metric_cols[3]: | |
fig = go.Figure(go.Indicator( | |
mode = "gauge+number", | |
value = 92.9, | |
domain = {'x': [0, 1], 'y': [0, 1]}, | |
title = {'text': "F1-Score", 'font': {'size': 24}}, | |
gauge = { | |
'axis': {'range': [None, 100], 'tickwidth': 1, 'tickcolor': "darkblue"}, | |
'bar': {'color': "#3B82F6"}, | |
'bgcolor': "white", | |
'borderwidth': 2, | |
'bordercolor': "gray", | |
'steps': [ | |
{'range': [0, 70], 'color': '#FFEDD5'}, | |
{'range': [70, 85], 'color': '#FEF3C7'}, | |
{'range': [85, 100], 'color': '#ECFDF5'}], | |
} | |
)) | |
fig.update_layout(height=200, margin=dict(l=20, r=20, t=50, b=20)) | |
st.plotly_chart(fig, use_container_width=True) | |
# Matrice de confusion | |
st.markdown('<h3 class="section-title">Matrice de confusion</h3>', unsafe_allow_html=True) | |
# Créer les onglets pour choisir le type de visualisation | |
matrix_tab1, matrix_tab2 = st.tabs(["Heatmap interactive", "Image statique"]) | |
with matrix_tab1: | |
# Créer une matrice de confusion fictive (similaire à celle montrée dans le rapport) | |
intent_labels = [ | |
"consulter_solde", "reclamer_facture", "declarer_panne", | |
"info_forfait", "recuperer_mot_de_passe", "salutations", | |
"remerciements", "demander_agent_humain", "hors_scope" | |
] | |
# Matrice fictive (similaire à celle montrée dans le rapport) | |
conf_matrix = np.array([ | |
[184, 0, 0, 3, 0, 0, 0, 0, 8], | |
[1, 130, 0, 2, 0, 0, 0, 2, 3], | |
[0, 2, 118, 0, 0, 0, 0, 6, 5], | |
[2, 3, 0, 121, 0, 0, 0, 0, 2], | |
[0, 0, 0, 0, 121, 0, 0, 2, 2], | |
[1, 0, 0, 0, 0, 109, 5, 0, 7], | |
[0, 0, 0, 0, 0, 3, 133, 0, 0], | |
[0, 0, 7, 0, 3, 0, 0, 111, 4], | |
[4, 2, 4, 0, 2, 9, 0, 5, 107] | |
]) | |
# Créer un DataFrame pour Plotly | |
matrix_data = [] | |
for i in range(len(intent_labels)): | |
for j in range(len(intent_labels)): | |
matrix_data.append({ | |
'Réelle': intent_labels[i], | |
'Prédite': intent_labels[j], | |
'Valeur': conf_matrix[i, j] | |
}) | |
df_conf = pd.DataFrame(matrix_data) | |
# Créer la heatmap avec Plotly | |
fig = px.density_heatmap( | |
df_conf, | |
x='Prédite', | |
y='Réelle', | |
z='Valeur', | |
color_continuous_scale='Blues', | |
text_auto=True | |
) | |
fig.update_layout( | |
title='Matrice de Confusion Interactive', | |
width=800, | |
height=600, | |
xaxis_title='Intention Prédite', | |
yaxis_title='Intention Réelle' | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
with matrix_tab2: | |
try: | |
confusion_img = Image.open("images/image8.jpg") | |
st.image(confusion_img, caption="Matrice de Confusion pour la Classification d'Intents en Darija") | |
except: | |
st.warning("Image de matrice de confusion non trouvée. Placez 'image8.jpg' dans le dossier 'images/'.") | |
# Créer une heatmap avec Matplotlib | |
fig, ax = plt.subplots(figsize=(10, 8)) | |
im = ax.imshow(conf_matrix, cmap='Blues') | |
# Étiquettes des axes | |
ax.set_xticks(np.arange(len(intent_labels))) | |
ax.set_yticks(np.arange(len(intent_labels))) | |
ax.set_xticklabels(intent_labels, rotation=45, ha="right") | |
ax.set_yticklabels(intent_labels) | |
# Ajout des valeurs dans les cellules | |
for i in range(len(intent_labels)): | |
for j in range(len(intent_labels)): | |
text = ax.text(j, i, conf_matrix[i, j], | |
ha="center", va="center", color="black" if conf_matrix[i, j] < 100 else "white") | |
ax.set_xlabel('Intention prédite') | |
ax.set_ylabel('Intention réelle') | |
ax.set_title('Matrice de Confusion') | |
fig.tight_layout() | |
st.pyplot(fig) | |
# Performance par intention | |
st.markdown('<h3 class="section-title">Performance par intention</h3>', unsafe_allow_html=True) | |
perf_tab1, perf_tab2 = st.tabs(["Graphique interactif", "Image statique"]) | |
with perf_tab1: | |
# Créer un graphique interactif avec Plotly | |
intents = [ | |
"consulter_solde", "reclamer_facture", "declarer_panne", | |
"info_forfait", "recuperer_mot_de_passe", "salutations", | |
"remerciements", "demander_agent_humain", "hors_scope" | |
] | |
# Données (similaires à celles du rapport) | |
precision = [0.981, 0.949, 0.907, 0.887, 0.947, 0.906, 0.964, 0.867, 0.847] | |
recall = [0.943, 0.944, 0.904, 0.945, 0.967, 0.890, 0.978, 0.931, 0.807] | |
f1 = [0.962, 0.946, 0.905, 0.915, 0.957, 0.898, 0.971, 0.898, 0.827] | |
# Créer un DataFrame pour Plotly | |
df_perf = pd.DataFrame({ | |
'Intention': intents * 3, | |
'Métrique': ['Précision'] * len(intents) + ['Rappel'] * len(intents) + ['F1-Score'] * len(intents), | |
'Valeur': precision + recall + f1 | |
}) | |
# Créer le graphique avec Plotly | |
fig = px.bar( | |
df_perf, | |
x='Intention', | |
y='Valeur', | |
color='Métrique', | |
barmode='group', | |
color_discrete_map={ | |
'Précision': '#1f77b4', | |
'Rappel': '#ff7f0e', | |
'F1-Score': '#2ca02c' | |
}, | |
hover_data={'Intention': True, 'Métrique': True, 'Valeur': ':.3f'}, | |
title='Performance par intention' | |
) | |
fig.update_layout( | |
yaxis=dict( | |
title='Score', | |
range=[0.7, 1] | |
), | |
xaxis_title='', | |
legend_title='Métrique', | |
height=500 | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
with perf_tab2: | |
try: | |
perf_img = Image.open("images/image11.jpg") | |
st.image(perf_img, caption="Performance du Modèle par Intent (Précision, Rappel, F1-Score)") | |
except: | |
st.warning("Image de performance par intent non trouvée. Placez 'image11.jpg' dans le dossier 'images/'.") | |
# Créer un graphique avec Matplotlib | |
fig, ax = plt.subplots(figsize=(12, 6)) | |
x = np.arange(len(intents)) | |
width = 0.25 | |
ax.bar(x - width, precision, width, label='Précision', color='#1f77b4') | |
ax.bar(x, recall, width, label='Rappel', color='#ff7f0e') | |
ax.bar(x + width, f1, width, label='F1-Score', color='#2ca02c') | |
ax.set_ylabel('Score') | |
ax.set_title('Performance par intention') | |
ax.set_xticks(x) | |
ax.set_xticklabels(intents, rotation=45, ha='right') | |
ax.legend() | |
ax.set_ylim([0.7, 1]) | |
fig.tight_layout() | |
st.pyplot(fig) | |
# Évolution de l'entraînement | |
st.markdown('<h3 class="section-title">Évolution de l\'entraînement</h3>', unsafe_allow_html=True) | |
train_tab1, train_tab2 = st.tabs(["Graphique interactif", "Image statique"]) | |
with train_tab1: | |
# Créer un graphique interactif avec Plotly | |
steps = list(range(0, 1001, 50)) | |
train_loss = [4.5] + [4.5 * np.exp(-0.005 * step) + 0.3 + 0.1 * np.random.random() for step in steps[1:]] | |
val_loss = [4.2] + [4.2 * np.exp(-0.005 * step) + 0.35 + 0.15 * np.random.random() for step in steps[1:]] | |
# Créer un DataFrame | |
df_loss = pd.DataFrame({ | |
'Step': steps, | |
'Train Loss': train_loss, | |
'Val Loss': val_loss | |
}) | |
# Convertir en format long pour Plotly | |
df_loss_long = pd.melt( | |
df_loss, | |
id_vars=['Step'], | |
value_vars=['Train Loss', 'Val Loss'], | |
var_name='Type', | |
value_name='Loss' | |
) | |
# Créer le graphique avec Plotly | |
fig = px.line( | |
df_loss_long, | |
x='Step', | |
y='Loss', | |
color='Type', | |
title='Évolution de la perte durant l\'entraînement', | |
color_discrete_map={ | |
'Train Loss': 'blue', | |
'Val Loss': 'orange' | |
} | |
) | |
fig.update_layout( | |
xaxis_title='Étapes', | |
yaxis_title='Perte (Loss)', | |
height=400 | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
with train_tab2: | |
try: | |
training_img = Image.open("images/image5.jpg") | |
st.image(training_img, caption="Évolution de la Perte (Loss) durant l'Entraînement") | |
except: | |
st.warning("Image d'évolution de l'entraînement non trouvée. Placez 'image5.jpg' dans le dossier 'images/'.") | |
# Créer un graphique avec Matplotlib | |
fig, ax = plt.subplots(figsize=(10, 5)) | |
ax.plot(steps, train_loss, label='Train Loss', color='blue') | |
ax.plot(steps, val_loss, label='Val Loss', color='orange') | |
ax.set_xlabel('Étapes') | |
ax.set_ylabel('Perte (Loss)') | |
ax.set_title('Évolution de la perte durant l\'entraînement') | |
ax.legend() | |
ax.grid(True, linestyle='--', alpha=0.7) | |
fig.tight_layout() | |
st.pyplot(fig) | |
# Benchmarks et comparaisons | |
st.markdown('<h3 class="section-title">Benchmarks et comparaisons</h3>', unsafe_allow_html=True) | |
st.markdown(""" | |
<table class="styled-table"> | |
<thead> | |
<tr> | |
<th>Modèle</th> | |
<th>Accuracy</th> | |
<th>F1-Score</th> | |
<th>Temps de réponse</th> | |
<th>Taille</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td><strong>MARBERTv2 (notre approche)</strong></td> | |
<td>92.8%</td> | |
<td>92.9%</td> | |
<td>127ms</td> | |
<td>470MB</td> | |
</tr> | |
<tr> | |
<td>AraBERT</td> | |
<td>89.3%</td> | |
<td>89.1%</td> | |
<td>132ms</td> | |
<td>543MB</td> | |
</tr> | |
<tr> | |
<td>QARiB</td> | |
<td>87.5%</td> | |
<td>87.2%</td> | |
<td>145ms</td> | |
<td>420MB</td> | |
</tr> | |
<tr> | |
<td>BERT Multilingue</td> | |
<td>85.1%</td> | |
<td>84.9%</td> | |
<td>121ms</td> | |
<td>680MB</td> | |
</tr> | |
<tr> | |
<td>SVM + TF-IDF</td> | |
<td>78.6%</td> | |
<td>77.9%</td> | |
<td>65ms</td> | |
<td>25MB</td> | |
</tr> | |
</tbody> | |
</table> | |
""", unsafe_allow_html=True) | |
st.markdown(""" | |
<div class="info-box" style="background-color: #F8FAFC; border: 2px solid #E2E8F0; color: #1A202C; font-size: 16px; line-height: 1.6;"> | |
<p style="margin-bottom: 15px; font-weight: 500;">Notre approche basée sur <strong style="color: #1E3A8A; font-weight: 700;">MARBERTv2</strong> surpasse significativement les autres modèles, en particulier pour les intentions liées aux spécificités dialectales de la Darija et au code-switching.</p> | |
<p style="margin-bottom: 10px; font-weight: 600; color: #2D3748;">Les avantages de notre approche:</p> | |
<ul style="margin-left: 20px; color: #4A5568;"> | |
<li style="margin-bottom: 8px; font-weight: 500;">Meilleure gestion des variations dialectales régionales</li> | |
<li style="margin-bottom: 8px; font-weight: 500;">Support du code-switching entre Darija et Français</li> | |
<li style="margin-bottom: 8px; font-weight: 500;">Bonne performance sur les expressions idiomatiques spécifiques</li> | |
<li style="margin-bottom: 8px; font-weight: 500;">Équilibre optimal entre performance et temps de réponse</li> | |
</ul> | |
</div> | |
""", unsafe_allow_html=True) | |
# Onglet Architecture | |
with tab3: | |
st.markdown('<h2 class="sub-title slide-in">Architecture de la solution</h2>', unsafe_allow_html=True) | |
# Description de l'architecture | |
st.info(""" | |
**Cette section présente l'architecture technique de notre solution et son intégration avec la plateforme AICC.** | |
Notre système est conçu comme une API RESTful déployée sur Hugging Face Spaces, permettant une intégration flexible avec différentes plateformes de service client, dont la solution AICC de Huawei. | |
""") | |
# Architecture globale | |
st.markdown('<h3 class="section-title">Architecture globale</h3>', unsafe_allow_html=True) | |
arch_tab1, arch_tab2 = st.tabs(["Diagramme interactif", "Image statique"]) | |
with arch_tab1: | |
# Créer un diagramme d'architecture professionnel avec Plotly | |
fig = go.Figure() | |
# Définir une palette de couleurs professionnelle (dégradé de bleus) | |
colors = { | |
"Client": "#1E40AF", # Bleu foncé | |
"AICC": "#2563EB", # Bleu royal | |
"API Darija NLU": "#3B82F6", # Bleu moyen | |
"MARBERTv2": "#60A5FA", # Bleu clair | |
"Agents": "#1D4ED8", # Bleu profond | |
"Call Center": "#1E3A8A" # Bleu très foncé | |
} | |
# Repositionner les nœuds pour une meilleure présentation | |
nodes = [ | |
{"name": "Client", "x": 0, "y": 0, "size": 80, "icon": "👤"}, | |
{"name": "AICC\nPlateforme", "x": 2, "y": 0, "size": 90, "icon": "🏢"}, | |
{"name": "API Darija\nNLU", "x": 4, "y": 0, "size": 85, "icon": "🔗"}, | |
{"name": "MARBERTv2\nModèle", "x": 6, "y": 0, "size": 75, "icon": "🧠"}, | |
{"name": "Agents\nHumains", "x": 2, "y": -1.5, "size": 70, "icon": "👨💼"}, | |
{"name": "Call Center\nSupport", "x": 3, "y": -2.5, "size": 65, "icon": "📞"} | |
] | |
# Ajouter les nœuds avec des styles améliorés | |
for i, node in enumerate(nodes): | |
# Simplifier la logique de couleur | |
node_name = node["name"].replace("\n", " ") | |
if "Client" in node_name: | |
node_color = colors["Client"] | |
elif "AICC" in node_name: | |
node_color = colors["AICC"] | |
elif "API" in node_name or "Darija" in node_name: | |
node_color = colors["API Darija NLU"] | |
elif "MARBERT" in node_name or "Modèle" in node_name: | |
node_color = colors["MARBERTv2"] | |
elif "Agents" in node_name: | |
node_color = colors["Agents"] | |
elif "Call" in node_name or "Center" in node_name: | |
node_color = colors["Call Center"] | |
else: | |
node_color = "#3B82F6" # Couleur par défaut | |
fig.add_trace(go.Scatter( | |
x=[node["x"]], | |
y=[node["y"]], | |
mode="markers+text", | |
marker=dict( | |
size=node["size"], | |
color=node_color, | |
line=dict(width=3, color="white"), | |
opacity=0.9 | |
), | |
text=f'{node["icon"]}<br>{node["name"]}', | |
textposition="middle center", | |
textfont=dict(color="white", size=11, family="Arial Black"), | |
hoverinfo="text", | |
hovertext=f"<b>{node['name']}</b><br>Composant de l'architecture", | |
hoverlabel=dict(bgcolor="rgba(0,0,0,0.8)", font_color="white"), | |
showlegend=False | |
)) | |
# Définir les connexions avec des descriptions plus détaillées | |
edges = [ | |
{"from": 0, "to": 1, "label": "Requête client\n(Darija)", "color": "#2563EB", "style": "solid"}, | |
{"from": 1, "to": 2, "label": "API Call\n(HTTPS/POST)", "color": "#3B82F6", "style": "solid"}, | |
{"from": 2, "to": 3, "label": "Inférence ML\n(Tokenization)", "color": "#60A5FA", "style": "solid"}, | |
{"from": 3, "to": 2, "label": "Prédiction\n(Intent + Score)", "color": "#60A5FA", "style": "dash"}, | |
{"from": 2, "to": 1, "label": "Réponse JSON\n(Structured)", "color": "#3B82F6", "style": "dash"}, | |
{"from": 1, "to": 0, "label": "Réponse adaptée\n(Interface)", "color": "#2563EB", "style": "dash"}, | |
{"from": 1, "to": 4, "label": "Transfert\n(Si nécessaire)", "color": "#1D4ED8", "style": "dot"}, | |
{"from": 4, "to": 5, "label": "Escalade\n(Support)", "color": "#1E3A8A", "style": "solid"}, | |
{"from": 5, "to": 0, "label": "Support avancé\n(Humain)", "color": "#1E3A8A", "style": "solid"} | |
] | |
# Ajouter les connexions avec des styles variés | |
for edge in edges: | |
fig.add_shape( | |
type="line", | |
x0=nodes[edge["from"]]["x"], | |
y0=nodes[edge["from"]]["y"], | |
x1=nodes[edge["to"]]["x"], | |
y1=nodes[edge["to"]]["y"], | |
line=dict( | |
color=edge["color"], | |
width=3, | |
dash=edge["style"] | |
), | |
xref="x", | |
yref="y" | |
) | |
# Ajouter des flèches pour indiquer la direction | |
if edge["style"] != "dot": # Pas de flèche pour les connexions conditionnelles | |
# Calculer la position de la flèche | |
x0, y0 = nodes[edge["from"]]["x"], nodes[edge["from"]]["y"] | |
x1, y1 = nodes[edge["to"]]["x"], nodes[edge["to"]]["y"] | |
# Position de la flèche (75% du chemin) | |
arrow_x = x0 + 0.75 * (x1 - x0) | |
arrow_y = y0 + 0.75 * (y1 - y0) | |
fig.add_trace(go.Scatter( | |
x=[arrow_x], | |
y=[arrow_y], | |
mode="markers", | |
marker=dict( | |
symbol="arrow-right", | |
size=15, | |
color=edge["color"], | |
line=dict(width=1, color="white") | |
), | |
hoverinfo="skip", | |
showlegend=False | |
)) | |
# Ajouter les étiquettes des connexions | |
midpoint_x = (nodes[edge["from"]]["x"] + nodes[edge["to"]]["x"]) / 2 | |
midpoint_y = (nodes[edge["from"]]["y"] + nodes[edge["to"]]["y"]) / 2 | |
# Ajouter les étiquettes des connexions sans fond | |
fig.add_trace(go.Scatter( | |
x=[midpoint_x], | |
y=[midpoint_y], | |
mode="text", | |
text=edge["label"], | |
textposition="middle center", | |
textfont=dict( | |
size=9, | |
color=edge["color"], | |
family="Arial" | |
), | |
hoverinfo="text", | |
hovertext=f"<b>Flux:</b> {edge['label']}", | |
hoverlabel=dict(bgcolor="rgba(0,0,0,0.8)", font_color="white"), | |
showlegend=False | |
)) | |
# Configuration avancée de la mise en page | |
fig.update_layout( | |
title={ | |
'text': "🏗️ Architecture d'Intégration - API NLU Darija avec AICC", | |
'x': 0.5, | |
'xanchor': 'center', | |
'font': {'size': 18, 'color': '#1E3A8A', 'family': 'Arial Black'} | |
}, | |
showlegend=False, | |
hovermode="closest", | |
height=500, | |
margin=dict(t=80, b=40, l=40, r=40), | |
xaxis=dict( | |
showgrid=False, | |
zeroline=False, | |
showticklabels=False, | |
range=[-0.5, 6.5] | |
), | |
yaxis=dict( | |
showgrid=False, | |
zeroline=False, | |
showticklabels=False, | |
range=[-3, 1] | |
), | |
plot_bgcolor="rgba(248,250,252,0.3)", | |
paper_bgcolor="white", | |
font=dict(family="Arial, sans-serif") | |
) | |
# Ajouter une légende personnalisée | |
fig.add_annotation( | |
text="<b>Légende:</b><br>" + | |
"━━━ Flux principal<br>" + | |
"┄┄┄ Réponse<br>" + | |
"••••• Transfert conditionnel", | |
xref="paper", yref="paper", | |
x=0.02, y=0.02, | |
xanchor="left", yanchor="bottom", | |
showarrow=False, | |
font=dict(size=10, color="#1E3A8A"), | |
bgcolor="rgba(255,255,255,0.9)", | |
bordercolor="#E5E7EB", | |
borderwidth=1 | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
# Ajouter des métriques de performance de l'architecture | |
st.markdown("### 📊 Métriques de Performance de l'Architecture") | |
perf_cols = st.columns(4) | |
with perf_cols[0]: | |
st.metric( | |
label="⚡ Latence Moyenne", | |
value="127ms", | |
delta="-23ms vs baseline", | |
delta_color="inverse" | |
) | |
with perf_cols[1]: | |
st.metric( | |
label="🎯 Disponibilité", | |
value="99.8%", | |
delta="+0.3% ce mois", | |
delta_color="normal" | |
) | |
with perf_cols[2]: | |
st.metric( | |
label="🔄 Requêtes/sec", | |
value="1,250", | |
delta="+15% capacité", | |
delta_color="normal" | |
) | |
with perf_cols[3]: | |
st.metric( | |
label="🛡️ Taux d'erreur", | |
value="0.2%", | |
delta="-0.1% amélioration", | |
delta_color="inverse" | |
) | |
with arch_tab2: | |
try: | |
arch_img = Image.open("images/image17.jpg") | |
st.image(arch_img, caption="Diagramme d'architecture de l'intégration avec AICC") | |
except: | |
st.warning("Image d'architecture non trouvée. Placez 'image17.jpg' dans le dossier 'images/'.") | |
st.markdown(""" | |
``` | |
┌───────────────┐ ┌───────────────┐ ┌───────────────┐ | |
│ Client │ │ Plateforme │ │ API NLU │ | |
│ (Mobile/Web)│◄────►│ AICC Huawei │◄────►│ Darija │ | |
└───────────────┘ └───────────────┘ └───────────────┘ | |
▲ ▲ | |
│ │ | |
▼ │ | |
┌────────────┐ │ | |
│ Agents │ │ | |
│ Humains │ │ | |
└────────────┘ │ | |
▲ │ | |
│ │ | |
▼ ▼ | |
┌─────────────────────────────────┐ | |
│ Système de Gestion de │ | |
│ Centre de Contact │ | |
└─────────────────────────────────┘ | |
``` | |
""") | |
# Flux de traitement | |
st.markdown('<h3 class="section-title">Flux de traitement</h3>', unsafe_allow_html=True) | |
flow_tab1, flow_tab2 = st.tabs(["Séquence interactive", "Image statique"]) | |
with flow_tab1: | |
# Créer un diagramme de séquence professionnel | |
sequence_steps = [ | |
{"from": "Client", "to": "AICC", "message": "📱 Message Darija\n(Requête utilisateur)", "time": 1, "type": "request"}, | |
{"from": "AICC", "to": "API NLU", "message": "🔗 API Call HTTPS\n(POST /predict)", "time": 2, "type": "api"}, | |
{"from": "API NLU", "to": "MARBERTv2", "message": "🧠 Inférence ML\n(Tokenization)", "time": 3, "type": "ml"}, | |
{"from": "MARBERTv2", "to": "API NLU", "message": "🎯 Prédiction\n(Intent + Confidence)", "time": 4, "type": "response"}, | |
{"from": "API NLU", "to": "AICC", "message": "📊 Réponse JSON\n(Structured Data)", "time": 5, "type": "response"}, | |
{"from": "AICC", "to": "Client", "message": "✅ Réponse adaptée\n(Interface utilisateur)", "time": 6, "type": "response"} | |
] | |
# Liste des acteurs avec couleurs et icônes | |
actors = [ | |
{"name": "Client", "color": "#1E40AF", "icon": "👤"}, | |
{"name": "AICC Platform", "color": "#2563EB", "icon": "🏢"}, | |
{"name": "API NLU Darija", "color": "#3B82F6", "icon": "🔗"}, | |
{"name": "MARBERTv2 Model", "color": "#60A5FA", "icon": "🧠"} | |
] | |
# Création du diagramme de séquence | |
fig = go.Figure() | |
# Couleurs selon le type de message | |
message_colors = { | |
"request": "#1E40AF", | |
"api": "#2563EB", | |
"ml": "#3B82F6", | |
"response": "#60A5FA" | |
} | |
# Lignes de vie avec style professionnel | |
for i, actor in enumerate(actors): | |
# Ligne de vie | |
fig.add_trace(go.Scatter( | |
x=[i, i], | |
y=[0.5, -7], | |
mode="lines", | |
line=dict(color=actor["color"], width=3, dash="dot"), | |
opacity=0.6, | |
hoverinfo="none", | |
showlegend=False | |
)) | |
# En-tête des acteurs avec style moderne | |
fig.add_trace(go.Scatter( | |
x=[i], | |
y=[0.5], | |
mode="markers+text", | |
marker=dict( | |
size=60, | |
color=actor["color"], | |
line=dict(width=3, color="white"), | |
opacity=0.9 | |
), | |
text=f'{actor["icon"]}<br><b>{actor["name"]}</b>', | |
textposition="middle center", | |
textfont=dict(color="white", size=10, family="Arial Bold"), | |
hoverinfo="text", | |
hovertext=f"<b>{actor['name']}</b><br>Composant système", | |
hoverlabel=dict(bgcolor="rgba(0,0,0,0.8)", font_color="white"), | |
showlegend=False | |
)) | |
# Ajouter les messages avec styles différenciés | |
for step in sequence_steps: | |
# Trouver l'index des acteurs correspondants de manière plus flexible | |
from_idx = -1 | |
to_idx = -1 | |
# Recherche plus robuste pour les acteurs sources et destinations | |
for i, actor in enumerate(actors): | |
actor_name = actor["name"] | |
# Vérifier si l'acteur correspond à l'acteur source | |
if step["from"] in actor_name or actor_name.split()[0] == step["from"]: | |
from_idx = i | |
# Vérifier si l'acteur correspond à l'acteur destination | |
if step["to"] in actor_name or actor_name.split()[0] == step["to"]: | |
to_idx = i | |
# Si on n'a pas trouvé les acteurs, utiliser une approche plus générique | |
if from_idx == -1: | |
from_idx = 0 # Utiliser le premier acteur par défaut | |
print(f"Acteur source non trouvé pour {step['from']}") | |
if to_idx == -1: | |
to_idx = 1 # Utiliser le deuxième acteur par défaut | |
print(f"Acteur destination non trouvé pour {step['to']}") | |
time_y = -step["time"] | |
color = message_colors[step["type"]] | |
# Flèche du message avec direction | |
if from_idx < to_idx: # Message vers la droite | |
arrow_symbol = "triangle-right" | |
x_positions = [from_idx + 0.1, to_idx - 0.1] | |
else: # Message vers la gauche | |
arrow_symbol = "triangle-left" | |
x_positions = [from_idx - 0.1, to_idx + 0.1] | |
# Ligne de message | |
fig.add_shape( | |
type="line", | |
x0=x_positions[0], | |
y0=time_y, | |
x1=x_positions[1], | |
y1=time_y, | |
line=dict(color=color, width=3), | |
xref="x", | |
yref="y" | |
) | |
# Flèche de direction | |
fig.add_trace(go.Scatter( | |
x=[x_positions[1]], | |
y=[time_y], | |
mode="markers", | |
marker=dict( | |
symbol=arrow_symbol, | |
size=12, | |
color=color, | |
line=dict(width=1, color="white") | |
), | |
hoverinfo="skip", | |
showlegend=False | |
)) | |
# Étiquette du message avec fond | |
mid_x = (x_positions[0] + x_positions[1]) / 2 | |
fig.add_trace(go.Scatter( | |
x=[mid_x], | |
y=[time_y + 0.15], | |
mode="text", | |
text=step["message"], | |
textposition="middle center", | |
textfont=dict( | |
size=9, | |
color=color, | |
family="Arial" | |
), | |
hoverinfo="text", | |
hovertext=f"<b>Étape {step['time']}:</b><br>{step['message']}", | |
hoverlabel=dict(bgcolor="rgba(0,0,0,0.8)", font_color="white"), | |
showlegend=False | |
)) | |
# Ajouter un indicateur temporel | |
fig.add_trace(go.Scatter( | |
x=[-0.3], | |
y=[time_y], | |
mode="markers+text", | |
marker=dict(size=20, color="#E5E7EB", line=dict(width=1, color="#9CA3AF")), | |
text=f"{step['time']}", | |
textposition="middle center", | |
textfont=dict(size=10, color="#374151", family="Arial Bold"), | |
hoverinfo="text", | |
hovertext=f"Séquence {step['time']}", | |
showlegend=False | |
)) | |
# Configuration avancée de la mise en page | |
fig.update_layout( | |
title={ | |
'text': "🔄 Diagramme de Séquence - Flux de Traitement NLU", | |
'x': 0.5, | |
'xanchor': 'center', | |
'font': {'size': 18, 'color': '#1E3A8A', 'family': 'Arial Black'} | |
}, | |
showlegend=False, | |
hovermode="closest", | |
height=600, | |
margin=dict(t=80, b=40, l=80, r=40), | |
xaxis=dict( | |
showgrid=False, | |
zeroline=False, | |
showticklabels=False, | |
range=[-0.8, len(actors) - 0.2] | |
), | |
yaxis=dict( | |
showgrid=True, | |
gridcolor="rgba(0,0,0,0.1)", | |
zeroline=False, | |
showticklabels=False, | |
range=[-7.5, 1] | |
), | |
plot_bgcolor="rgba(248,250,252,0.3)", | |
paper_bgcolor="white", | |
font=dict(family="Arial, sans-serif") | |
) | |
# Ajouter une légende temporelle | |
fig.add_annotation( | |
text="<b>Chronologie:</b><br>" + | |
"① Requête initiale<br>" + | |
"② Appel API<br>" + | |
"③ Traitement ML<br>" + | |
"④ Résultat modèle<br>" + | |
"⑤ Réponse structurée<br>" + | |
"⑥ Interface utilisateur", | |
xref="paper", yref="paper", | |
x=0.02, y=0.98, | |
xanchor="left", yanchor="top", | |
showarrow=False, | |
font=dict(size=10, color="#1E3A8A"), | |
bgcolor="rgba(255,255,255,0.95)", | |
bordercolor="#E5E7EB", | |
borderwidth=1 | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
# Ajouter des informations sur la latence | |
st.markdown("### ⏱️ Analyse de Performance par Étape") | |
latency_cols = st.columns(6) | |
latencies = ["12ms", "8ms", "95ms", "5ms", "6ms", "1ms"] | |
steps_names = ["Requête", "Routage", "Inférence", "Post-process", "Réponse", "Affichage"] | |
for i, (col, latency, step_name) in enumerate(zip(latency_cols, latencies, steps_names)): | |
with col: | |
st.metric( | |
label=f"Étape {i+1}", | |
value=latency, | |
help=f"Latence moyenne pour: {step_name}" | |
) | |
with arch_tab2: | |
try: | |
arch_img = Image.open("images/image17.jpg") | |
st.image(arch_img, caption="Diagramme d'architecture de l'intégration avec AICC") | |
except: | |
st.warning("Image d'architecture non trouvée. Placez 'image17.jpg' dans le dossier 'images/'.") | |
# Structure de l'API | |
st.markdown('<h3 class="section-title">Structure de l\'API</h3>', unsafe_allow_html=True) | |
# Détails d'implémentation dans un expander | |
with st.expander("Détails d'implémentation", expanded=True): | |
st.markdown("#### FastAPI - Endpoint principal") | |
code_fastapi = ''' | |
@app.post("/predict", response_model=PredictionResponse, tags=["Prediction"]) | |
async def predict_intent(input_data: TextInput): | |
""" | |
Prédit l'intention d'un texte en Darija. | |
""" | |
try: | |
text = input_data.text.strip() | |
# Validation des entrées | |
if not text or len(text) < 2: | |
raise HTTPException( | |
status_code=400, | |
detail="Le texte d'entrée est vide ou trop court" | |
) | |
# Appel au service NLU | |
intent, confidence = await NLU_service.predict_intent(text) | |
return PredictionResponse( | |
intent=intent, | |
confidence=float(confidence) | |
) | |
except Exception as e: | |
# Gestion des erreurs | |
if isinstance(e, HTTPException): | |
raise e | |
else: | |
raise HTTPException( | |
status_code=500, | |
detail=f"Erreur lors de la prédiction: {str(e)}" | |
) | |
''' | |
st.code(code_fastapi, language="python") | |
st.markdown("#### Dockerfile") | |
code_dockerfile = ''' | |
# Étape 1: Utiliser une image de base Python officielle | |
FROM python:3.9-slim | |
# Étape 2: Définir le répertoire de travail dans le container | |
WORKDIR /app | |
# Étape 3: Copier le fichier des dépendances | |
COPY requirements.txt requirements.txt | |
# Étape 4: Installer les dépendances | |
# --no-cache-dir pour garder l'image légère | |
RUN pip install --no-cache-dir --upgrade pip | |
RUN pip install --no-cache-dir -r requirements.txt | |
# Étape 5: Copier tout le reste de votre projet dans le container | |
COPY . . | |
# Étape 6: Exposer le port que votre API utilise | |
EXPOSE 8000 | |
# Étape 7: La commande pour lancer l'API quand le container démarre | |
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] | |
''' | |
st.code(code_dockerfile, language="dockerfile") | |
# Section Déploiement | |
st.markdown('<h3 class="section-title">Déploiement</h3>', unsafe_allow_html=True) | |
deployment_tabs = st.tabs(["Hugging Face Spaces", "Intégration AICC", "Monitoring"]) | |
with deployment_tabs[0]: | |
st.markdown(""" | |
Notre API est déployée sur Hugging Face Spaces qui offre: | |
- Infrastructure évolutive | |
- Monitoring intégré | |
- Haute disponibilité | |
- Intégration CI/CD via Git | |
Le déploiement est automatiquement effectué à chaque push sur le dépôt GitHub. | |
""") | |
st.info("Notre API est déployée avec Hugging Face Spaces pour bénéficier d'une infrastructure évolutive et d'un déploiement continu.") | |
st.markdown("#### URL de l'API déployée") | |
st.markdown("[https://mediani-darija-aicc-api.hf.space](https://mediani-darija-aicc-api.hf.space)") | |
st.markdown("#### Documentation Swagger") | |
st.markdown("[https://mediani-darija-aicc-api.hf.space/docs](https://mediani-darija-aicc-api.hf.space/docs)") | |
with deployment_tabs[1]: | |
st.markdown(""" | |
**L'intégration avec la plateforme AICC de Huawei comprend:** | |
1. Configuration des webhooks pour les appels API | |
2. Adaptation des réponses JSON au format AICC | |
3. Mise en place d'une authentification sécurisée | |
4. Calibration des timeouts et des retry policies | |
Cette intégration permet d'enrichir les capacités de compréhension du langage naturel d'AICC avec notre modèle spécialisé pour la Darija. | |
""") | |
with deployment_tabs[2]: | |
st.markdown(""" | |
### Le monitoring de notre API inclut: | |
""") | |
monitoring_cols = st.columns(2) | |
with monitoring_cols[0]: | |
st.metric( | |
label="Temps de réponse", | |
value="127ms", | |
delta="-5ms" | |
) | |
st.metric( | |
label="Taux de disponibilité", | |
value="99.97%", | |
delta="+0.2%" | |
) | |
with monitoring_cols[1]: | |
st.metric( | |
label="Distribution des intentions", | |
value="9 catégories", | |
help="Répartition équilibrée entre les différentes intentions" | |
) | |
st.metric( | |
label="Alertes en cas d'anomalies", | |
value="Activées", | |
help="Système de détection d'anomalies en temps réel" | |
) | |
st.info("Les métriques sont collectées en temps réel et disponibles via un tableau de bord dédié.") | |
# Pied de page | |
st.markdown("---") | |
st.markdown("© 2025 Mohammed MEDIANI") |