import gradio as gr import torch import numpy as np from transformers import AutoTokenizer, AutoModelForSequenceClassification # ============================================================ # CONFIGURATION # ============================================================ MODEL_ID = "doris-sylvie/afro-xlmr-malagasy-sentiment" device = "cuda" if torch.cuda.is_available() else "cpu" ID_TO_LABEL = {0: "negatif", 1: "positif"} ID_TO_EMOJI = {0: "😔", 1: "😊"} ID_TO_COLOR = {0: "#ef4444", 1: "#22c55e"} # ============================================================ # CHARGEMENT DU MODÈLE # ============================================================ print(f"Chargement du modèle {MODEL_ID}...") tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) model = AutoModelForSequenceClassification.from_pretrained(MODEL_ID).to(device) model.eval() print("Modèle chargé.") # ============================================================ # FONCTION D'ANALYSE # ============================================================ def analyser_sentiment(texte): if not texte or not texte.strip(): return ( gr.update(value="—", visible=True), gr.update(value="—", visible=True), gr.update(value=0, visible=True), gr.update(value=0, visible=True), ) inputs = tokenizer( texte, return_tensors="pt", truncation=True, max_length=256, padding=True ).to(device) with torch.no_grad(): outputs = model(**inputs) probs = torch.softmax(outputs.logits, dim=-1)[0] pred_id = torch.argmax(probs).item() score_pos = round(probs[1].item() * 100, 1) score_neg = round(probs[0].item() * 100, 1) label = ID_TO_LABEL[pred_id] emoji = ID_TO_EMOJI[pred_id] resultat = f"{emoji} Sentiment **{label.upper()}** — confiance {max(score_pos, score_neg):.1f}%" return ( gr.update(value=resultat), gr.update(value=f"Positif : {score_pos}%"), gr.update(value=score_pos), gr.update(value=score_neg), ) # ============================================================ # EXEMPLES # ============================================================ EXEMPLES = [ ["Tsara be ny andro anio, faly aho satria nahita ny namako."], ["Ratsy ny vaovao avy any an-tany, manahirana ny fo."], ["Manao ahoana ianao androany?"], ["Dimy ao amin'ny faritra Diana."], ["Sambatra ny olona izay mahatoky, fa ho tanteraka ny tenin'Andriamanitra."], ["Kivy aho satria tsy nisy nanampy ahy tamin'ny andro sarotra."], ] # ============================================================ # INTERFACE GRADIO # ============================================================ CSS = """ .container { max-width: 820px; margin: 0 auto; } .titre-principal { font-family: 'Georgia', serif; font-size: 2rem; font-weight: 700; color: #1e293b; text-align: center; margin-bottom: 0.25rem; } .sous-titre { text-align: center; color: #64748b; font-size: 0.95rem; margin-bottom: 1.5rem; } .badge-modele { display: inline-block; background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 999px; padding: 2px 12px; font-size: 0.75rem; color: #475569; font-family: monospace; } .card-resultat { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 1.25rem; margin-top: 0.5rem; } .label-score { font-size: 0.8rem; color: #94a3b8; margin-bottom: 2px; } .section-label { font-size: 0.75rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: #94a3b8; margin-bottom: 6px; } footer { display: none !important; } """ with gr.Blocks(css=CSS, theme=gr.themes.Base()) as demo: # ── En-tête gr.HTML("""

DIAGuard — Analyse de Sentiment

Classifiez le sentiment d'un texte en Malagasy officiel
doris-sylvie/afro-xlmr-malagasy-sentiment

""") with gr.Column(elem_classes="container"): # ── Zone de saisie gr.HTML('

Texte à analyser

') texte_input = gr.Textbox( placeholder="Écrivez un texte en Malagasy officiel…", lines=3, max_lines=6, show_label=False, container=False ) btn = gr.Button( "Analyser le sentiment", variant="primary", size="lg" ) # ── Résultat principal gr.HTML('

Résultat

') with gr.Group(elem_classes="card-resultat"): resultat_md = gr.Markdown(value="*En attente d'un texte…*") with gr.Row(): with gr.Column(): gr.HTML('

😊 Score positif

') score_pos_bar = gr.Slider( minimum=0, maximum=100, value=0, interactive=False, show_label=False ) with gr.Column(): gr.HTML('

😔 Score négatif

') score_neg_bar = gr.Slider( minimum=0, maximum=100, value=0, interactive=False, show_label=False ) detail_md = gr.Markdown(value="") # ── Exemples gr.HTML('

Exemples

') gr.Examples( examples=EXEMPLES, inputs=[texte_input], label=None, examples_per_page=6, ) # ── Note méthodologique gr.HTML("""
Note méthodologique
Ce modèle est basé sur Afro-XLM-R (Davlan/afro-xlmr-base), fine-tuné sur le VMSC (Vaovao Malagasy Sentiment Corpus — 4 536 phrases, presse Malagasy). Accuracy : 84.75% | F1 : 0.8476 (évaluation sur 505 exemples, epoch 3/5).

Dans le pipeline DIAGuard, ce modèle reçoit du texte déjà traduit en Malagasy officiel par ByT5 depuis le Betsileo.
""") # ── Événements btn.click( fn=analyser_sentiment, inputs=[texte_input], outputs=[resultat_md, detail_md, score_pos_bar, score_neg_bar] ) texte_input.submit( fn=analyser_sentiment, inputs=[texte_input], outputs=[resultat_md, detail_md, score_pos_bar, score_neg_bar] ) if __name__ == "__main__": demo.launch() # import gradio as gr # import torch # from transformers import ( # AutoTokenizer, # AutoModelForSequenceClassification, # AutoModelForSeq2SeqLM, # ) # # ============================================================ # # CONFIGURATION # # ============================================================ # SENTIMENT_MODEL_ID = "doris-sylvie/afro-xlmr-malagasy-sentiment" # TRANSLATOR_MODEL_ID = "doris-sylvie/byt5-betsileo-malagasy" # DIRECTION_BETS_TO_MG = "translate Betsileo to Malagasy" # device = "cuda" if torch.cuda.is_available() else "cpu" # ID_TO_LABEL = {0: "negatif", 1: "positif"} # ID_TO_EMOJI = {0: "😔", 1: "😊"} # # ============================================================ # # CHARGEMENT DES MODÈLES # # ============================================================ # print(f"Chargement du traducteur ByT5 ({TRANSLATOR_MODEL_ID})...") # translator_tokenizer = AutoTokenizer.from_pretrained(TRANSLATOR_MODEL_ID) # translator = AutoModelForSeq2SeqLM.from_pretrained(TRANSLATOR_MODEL_ID).to(device) # translator.eval() # print("Traducteur ByT5 chargé.") # print(f"Chargement du classificateur ({SENTIMENT_MODEL_ID})...") # sentiment_tokenizer = AutoTokenizer.from_pretrained(SENTIMENT_MODEL_ID) # sentiment_model = AutoModelForSequenceClassification.from_pretrained( # SENTIMENT_MODEL_ID # ).to(device) # sentiment_model.eval() # print("Classificateur chargé.") # # ============================================================ # # TRADUCTION ByT5 (Betsileo → Malagasy officiel) # # ============================================================ # def traduire_betsileo_vers_malagasy(texte: str) -> str: # if not texte or not texte.strip(): # return texte # inputs = translator_tokenizer( # f"{DIRECTION_BETS_TO_MG}: {texte}", # return_tensors="pt", # truncation=True, # max_length=512, # ).to(device) # with torch.no_grad(): # outputs = translator.generate( # **inputs, # max_length=256, # num_beams=4, # no_repeat_ngram_size=3, # early_stopping=True, # ) # return translator_tokenizer.decode(outputs[0], skip_special_tokens=True) # # ============================================================ # # CLASSIFICATION DE SENTIMENT (Malagasy officiel) # # ============================================================ # def classer_sentiment(texte_mg: str): # inputs = sentiment_tokenizer( # texte_mg, # return_tensors="pt", # truncation=True, # max_length=256, # padding=True, # ).to(device) # with torch.no_grad(): # outputs = sentiment_model(**inputs) # probs = torch.softmax(outputs.logits, dim=-1)[0] # pred_id = torch.argmax(probs).item() # score_pos = round(probs[1].item() * 100, 1) # score_neg = round(probs[0].item() * 100, 1) # label = ID_TO_LABEL[pred_id] # emoji = ID_TO_EMOJI[pred_id] # confiance = max(score_pos, score_neg) # resultat = ( # f"{emoji} Sentiment **{label.upper()}** — confiance {confiance:.1f}%" # ) # detail = f"Positif : {score_pos}% · Négatif : {score_neg}%" # return resultat, detail, score_pos, score_neg # # ============================================================ # # PIPELINE COMPLET # # ============================================================ # def analyser_sentiment(texte: str): # vide = ( # gr.update(value="—"), # gr.update(value="*En attente d'un texte…*"), # gr.update(value=""), # gr.update(value=0), # gr.update(value=0), # ) # if not texte or not texte.strip(): # return vide # # 1. Betsileo → Malagasy officiel (ByT5) # texte_mg = traduire_betsileo_vers_malagasy(texte) # # 2. Classification sur le Malagasy officiel # resultat, detail, score_pos, score_neg = classer_sentiment(texte_mg) # return ( # gr.update(value=texte_mg), # gr.update(value=resultat), # gr.update(value=detail), # gr.update(value=score_pos), # gr.update(value=score_neg), # ) # # ============================================================ # # EXEMPLES (Betsileo) # # ============================================================ # EXEMPLES = [ # ["Akory anao io? Tsara be ny andro anio."], # ["Ratsy ny vaovao, manahirana ny fo."], # ["Kivy aho satria tsy nisy nanampy ahy."], # ["Sambatra ny olona izay mahatoky."], # ["Manao ahoana ianao androany?"], # ["Faly aho satria nahita ny namako."], # ] # # ============================================================ # # INTERFACE GRADIO # # ============================================================ # CSS = """ # .container { max-width: 820px; margin: 0 auto; } # .titre-principal { # font-family: 'Georgia', serif; # font-size: 2rem; # font-weight: 700; # color: #1e293b; # text-align: center; # margin-bottom: 0.25rem; # } # .sous-titre { # text-align: center; # color: #64748b; # font-size: 0.95rem; # margin-bottom: 1.5rem; # } # .badge-modele { # display: inline-block; # background: #f1f5f9; # border: 1px solid #e2e8f0; # border-radius: 999px; # padding: 2px 12px; # font-size: 0.75rem; # color: #475569; # font-family: monospace; # margin: 0 2px; # } # .card-resultat { # background: #f8fafc; # border: 1px solid #e2e8f0; # border-radius: 12px; # padding: 1.25rem; # margin-top: 0.5rem; # } # .label-score { # font-size: 0.8rem; # color: #94a3b8; # margin-bottom: 2px; # } # .section-label { # font-size: 0.75rem; # font-weight: 600; # letter-spacing: 0.08em; # text-transform: uppercase; # color: #94a3b8; # margin-bottom: 6px; # } # footer { display: none !important; } # """ # with gr.Blocks(css=CSS, theme=gr.themes.Base()) as demo: # gr.HTML(""" #
#

DIAGuard — Analyse de Sentiment

#

# Pipeline Betsileo → Malagasy officiel → Sentiment
# doris-sylvie/byt5-betsileo-malagasy # doris-sylvie/afro-xlmr-malagasy-sentiment #

#
# """) # with gr.Column(elem_classes="container"): # gr.HTML('

Texte en Betsileo

') # texte_input = gr.Textbox( # placeholder="Écrivez un texte en dialecte Betsileo…", # lines=3, # max_lines=6, # show_label=False, # container=False, # ) # btn = gr.Button( # "Traduire & analyser le sentiment", # variant="primary", # size="lg", # ) # gr.HTML( # '

' # "Traduction Malagasy officiel (ByT5)

" # ) # traduction_md = gr.Textbox( # value="—", # lines=2, # max_lines=4, # interactive=False, # show_label=False, # container=False, # ) # gr.HTML( # '

Résultat

' # ) # with gr.Group(elem_classes="card-resultat"): # resultat_md = gr.Markdown(value="*En attente d'un texte…*") # with gr.Row(): # with gr.Column(): # gr.HTML('

😊 Score positif

') # score_pos_bar = gr.Slider( # minimum=0, # maximum=100, # value=0, # interactive=False, # show_label=False, # ) # with gr.Column(): # gr.HTML('

😔 Score négatif

') # score_neg_bar = gr.Slider( # minimum=0, # maximum=100, # value=0, # interactive=False, # show_label=False, # ) # detail_md = gr.Markdown(value="") # gr.HTML( # '

Exemples

' # ) # gr.Examples( # examples=EXEMPLES, # inputs=[texte_input], # label=None, # examples_per_page=6, # ) # gr.HTML(""" #
# Pipeline DIAGuard
# 1. ByT5 (byt5-betsileo-malagasy) traduit # le texte Betsileo en Malagasy officiel.
# 2. Afro-XLM-R (afro-xlmr-malagasy-sentiment) # classifie le sentiment sur cette traduction # (Accuracy 84.75% | F1 0.8476 sur VMSC).

# Le classificateur n'a pas été entraîné sur le Betsileo : # la traduction ByT5 est l'étape indispensable du pipeline. #
# """) # outputs = [ # traduction_md, # resultat_md, # detail_md, # score_pos_bar, # score_neg_bar, # ] # btn.click(fn=analyser_sentiment, inputs=[texte_input], outputs=outputs) # texte_input.submit( # fn=analyser_sentiment, inputs=[texte_input], outputs=outputs # ) # if __name__ == "__main__": # demo.launch()