Spaces:
Sleeping
Sleeping
| 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(""" | |
| <div class="container" style="padding-top:1.5rem; padding-bottom:0.5rem;"> | |
| <p class="titre-principal">DIAGuard — Analyse de Sentiment</p> | |
| <p class="sous-titre"> | |
| Classifiez le sentiment d'un texte en Malagasy officiel<br> | |
| <span class="badge-modele">doris-sylvie/afro-xlmr-malagasy-sentiment</span> | |
| </p> | |
| </div> | |
| """) | |
| with gr.Column(elem_classes="container"): | |
| # ── Zone de saisie | |
| gr.HTML('<p class="section-label">Texte à analyser</p>') | |
| 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('<p class="section-label" style="margin-top:1rem;">Résultat</p>') | |
| 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('<p class="label-score">😊 Score positif</p>') | |
| score_pos_bar = gr.Slider( | |
| minimum=0, maximum=100, | |
| value=0, interactive=False, | |
| show_label=False | |
| ) | |
| with gr.Column(): | |
| gr.HTML('<p class="label-score">😔 Score négatif</p>') | |
| score_neg_bar = gr.Slider( | |
| minimum=0, maximum=100, | |
| value=0, interactive=False, | |
| show_label=False | |
| ) | |
| detail_md = gr.Markdown(value="") | |
| # ── Exemples | |
| gr.HTML('<p class="section-label" style="margin-top:1.25rem;">Exemples</p>') | |
| gr.Examples( | |
| examples=EXEMPLES, | |
| inputs=[texte_input], | |
| label=None, | |
| examples_per_page=6, | |
| ) | |
| # ── Note méthodologique | |
| gr.HTML(""" | |
| <div style=" | |
| margin-top: 1.5rem; | |
| padding: 1rem 1.25rem; | |
| background: #fefce8; | |
| border: 1px solid #fde68a; | |
| border-radius: 10px; | |
| font-size: 0.82rem; | |
| color: #78350f; | |
| line-height: 1.6; | |
| "> | |
| <strong>Note méthodologique</strong><br> | |
| Ce modèle est basé sur <strong>Afro-XLM-R</strong> (Davlan/afro-xlmr-base), | |
| fine-tuné sur le <strong>VMSC</strong> (Vaovao Malagasy Sentiment Corpus — | |
| 4 536 phrases, presse Malagasy). | |
| Accuracy : <strong>84.75%</strong> | F1 : <strong>0.8476</strong> | |
| (évaluation sur 505 exemples, epoch 3/5).<br><br> | |
| Dans le pipeline DIAGuard, ce modèle reçoit du texte déjà traduit | |
| en Malagasy officiel par ByT5 depuis le Betsileo. | |
| </div> | |
| """) | |
| # ── É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(""" | |
| # <div class="container" style="padding-top:1.5rem; padding-bottom:0.5rem;"> | |
| # <p class="titre-principal">DIAGuard — Analyse de Sentiment</p> | |
| # <p class="sous-titre"> | |
| # Pipeline Betsileo → Malagasy officiel → Sentiment<br> | |
| # <span class="badge-modele">doris-sylvie/byt5-betsileo-malagasy</span> | |
| # <span class="badge-modele">doris-sylvie/afro-xlmr-malagasy-sentiment</span> | |
| # </p> | |
| # </div> | |
| # """) | |
| # with gr.Column(elem_classes="container"): | |
| # gr.HTML('<p class="section-label">Texte en Betsileo</p>') | |
| # 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( | |
| # '<p class="section-label" style="margin-top:1rem;">' | |
| # "Traduction Malagasy officiel (ByT5)</p>" | |
| # ) | |
| # traduction_md = gr.Textbox( | |
| # value="—", | |
| # lines=2, | |
| # max_lines=4, | |
| # interactive=False, | |
| # show_label=False, | |
| # container=False, | |
| # ) | |
| # gr.HTML( | |
| # '<p class="section-label" style="margin-top:1rem;">Résultat</p>' | |
| # ) | |
| # 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('<p class="label-score">😊 Score positif</p>') | |
| # score_pos_bar = gr.Slider( | |
| # minimum=0, | |
| # maximum=100, | |
| # value=0, | |
| # interactive=False, | |
| # show_label=False, | |
| # ) | |
| # with gr.Column(): | |
| # gr.HTML('<p class="label-score">😔 Score négatif</p>') | |
| # score_neg_bar = gr.Slider( | |
| # minimum=0, | |
| # maximum=100, | |
| # value=0, | |
| # interactive=False, | |
| # show_label=False, | |
| # ) | |
| # detail_md = gr.Markdown(value="") | |
| # gr.HTML( | |
| # '<p class="section-label" style="margin-top:1.25rem;">Exemples</p>' | |
| # ) | |
| # gr.Examples( | |
| # examples=EXEMPLES, | |
| # inputs=[texte_input], | |
| # label=None, | |
| # examples_per_page=6, | |
| # ) | |
| # gr.HTML(""" | |
| # <div style=" | |
| # margin-top: 1.5rem; | |
| # padding: 1rem 1.25rem; | |
| # background: #fefce8; | |
| # border: 1px solid #fde68a; | |
| # border-radius: 10px; | |
| # font-size: 0.82rem; | |
| # color: #78350f; | |
| # line-height: 1.6; | |
| # "> | |
| # <strong>Pipeline DIAGuard</strong><br> | |
| # 1. <strong>ByT5</strong> (<code>byt5-betsileo-malagasy</code>) traduit | |
| # le texte Betsileo en Malagasy officiel.<br> | |
| # 2. <strong>Afro-XLM-R</strong> (<code>afro-xlmr-malagasy-sentiment</code>) | |
| # classifie le sentiment sur cette traduction | |
| # (Accuracy 84.75% | F1 0.8476 sur VMSC).<br><br> | |
| # Le classificateur n'a pas été entraîné sur le Betsileo : | |
| # la traduction ByT5 est l'étape indispensable du pipeline. | |
| # </div> | |
| # """) | |
| # 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() | |