Spaces:
Sleeping
Sleeping
Commit ·
03a98ed
1
Parent(s): f1f081e
chore: ignore local model weights
Browse files- .gitattributes +1 -0
- .gitignore +1 -1
- README.md +58 -175
- app.py +195 -176
- data/model/bpo_bert_model/config.json +39 -0
- data/model/bpo_bert_model/tokenizer.json +0 -0
- data/model/bpo_bert_model/tokenizer_config.json +14 -0
- modules/bpo_dispatcher.py +192 -0
- requirements.txt +22 -7
.gitattributes
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
*.keras filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 3 |
multi-classification-tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 1 |
*.keras filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 3 |
multi-classification-tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
__pycache__/
|
|
|
|
| 1 |
+
__pycache__/data/model/bpo_bert_model/*.safetensors
|
README.md
CHANGED
|
@@ -1,213 +1,96 @@
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
| 3 |
title: ngt-ai-platform
|
| 4 |
-
sdk:
|
|
|
|
| 5 |
colorFrom: blue
|
| 6 |
colorTo: purple
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
# NGT AI Platform
|
| 11 |
|
| 12 |
-
|
| 13 |
-
1. binary classification di un testo fornito in input
|
| 14 |
-
2. image classification di una immagine fornita in input (Classi : Pneumonia, No_Pneumonia, Tubercolosi, No_Tubercolosi)
|
| 15 |
-
3. multilabel classification di un testo fornito in input (Classi: alt.atheism, comp.graphics, comp.os.ms-windows.misc, comp.sys.ibm.pc.hardware, comp.sys.mac.hardware, comp.windows.x, misc.forsale, rec.autos, rec.motorcycles, rec.sport.baseball, rec.sport.hockey, sci.crypt, sci.electronics, sci.med, sci.space, soc.religion.christian, talk.politics.guns, talk.politics.mideast, talk.politics.misc, talk.religion.misc)
|
| 16 |
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
La lemmatizzazione del testo viene eseguita con la libreria [spacy](https://spacy.io/usage).
|
| 23 |
-
Procedere con i seguenti passaggi
|
| 24 |
-
|
| 25 |
-
```bash
|
| 26 |
-
pip install -U pip setuptools wheel
|
| 27 |
-
pip install -U spacy
|
| 28 |
-
python -m spacy download it_core_news_lg
|
| 29 |
-
```
|
| 30 |
|
| 31 |
-
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
| 44 |
|
| 45 |
-
|
| 46 |
|
| 47 |
-
|
| 48 |
-
python -m pip install flask
|
| 49 |
-
```
|
| 50 |
|
| 51 |
-
|
| 52 |
|
|
|
|
| 53 |
```bash
|
| 54 |
-
|
|
|
|
| 55 |
```
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
```bash
|
| 60 |
-
|
|
|
|
|
|
|
| 61 |
```
|
| 62 |
|
| 63 |
-
|
|
|
|
|
|
|
| 64 |
|
| 65 |
```bash
|
| 66 |
-
|
| 67 |
```
|
|
|
|
| 68 |
|
| 69 |
-
|
| 70 |
-
1. binary classification all'indirizzo http://127.0.0.1:5000/binary-classification
|
| 71 |
-
2. image classification all'indirizzo http://127.0.0.1:5000/image-classification
|
| 72 |
-
3. multilabel classification all'indirizzo http://127.0.0.1:5000/multi-classification
|
| 73 |
|
|
|
|
| 74 |
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
|
| 78 |
-
1. text (required) -> contenente la sentence per cui si richiede la classificazione
|
| 79 |
-
2. model (optional) -> contenente il file del modello (.keras o .h5)
|
| 80 |
-
3. token (optional) -> contenente il file del tokenizer (.json)
|
| 81 |
|
| 82 |
-
|
| 83 |
|
| 84 |
-
```
|
| 85 |
-
|
| 86 |
-
"lemma": "che posto ragazzo ! uno cucina ricercare in piccolo cortile di altro tempo . bello , buone , bravissimo . prenotare con largo anticipo .",
|
| 87 |
-
"percent": "99.95895028114319",
|
| 88 |
-
"sentiment": "POSITIVE"
|
| 89 |
-
}
|
| 90 |
```
|
|
|
|
| 91 |
|
| 92 |
-
##
|
| 93 |
-
|
| 94 |
-
Effettuare una chiamata POST all'indirizzo indicato in precedenza. Il body dovrà essere in formato form-data con le seguenti property:
|
| 95 |
-
1. image (required) -> contenente il file per cui si richiede la classificazione
|
| 96 |
-
2. model (optional) -> contenente il file del modello (.keras o .h5)
|
| 97 |
-
|
| 98 |
-
La risposta sarà quindi
|
| 99 |
-
|
| 100 |
-
```json
|
| 101 |
-
[
|
| 102 |
-
{
|
| 103 |
-
"classe": "Tubercolosi",
|
| 104 |
-
"percent": "0.02414761"
|
| 105 |
-
},
|
| 106 |
-
{
|
| 107 |
-
"classe": "No_Tubercolosi",
|
| 108 |
-
"percent": "0.99304398"
|
| 109 |
-
},
|
| 110 |
-
{
|
| 111 |
-
"classe": "Pneumonia",
|
| 112 |
-
"percent": "0.00155318"
|
| 113 |
-
},
|
| 114 |
-
{
|
| 115 |
-
"classe": "No_Pneumonia",
|
| 116 |
-
"percent": "0.00484183"
|
| 117 |
-
}
|
| 118 |
-
]
|
| 119 |
-
```
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
Effettuare una chiamata POST all'indirizzo indicato in precedenza. Il body dovrà essere in formato form-data con le seguenti property:
|
| 124 |
-
1. text (required) -> contenente la sentence per cui si richiede la classificazione
|
| 125 |
-
2. model (optional) -> contenente il file del modello (.keras o .h5)
|
| 126 |
-
3. token (optional) -> contenente il file del tokenizer (.json)
|
| 127 |
-
|
| 128 |
-
La risposta sarà quindi
|
| 129 |
-
|
| 130 |
-
```json
|
| 131 |
-
[
|
| 132 |
-
{
|
| 133 |
-
"classe": "alt.atheism",
|
| 134 |
-
"percent": "20.58875114"
|
| 135 |
-
},
|
| 136 |
-
{
|
| 137 |
-
"classe": "comp.graphics",
|
| 138 |
-
"percent": "5.57006039"
|
| 139 |
-
},
|
| 140 |
-
{
|
| 141 |
-
"classe": "comp.os.ms-windows.misc",
|
| 142 |
-
"percent": "1.00294100"
|
| 143 |
-
},
|
| 144 |
-
{
|
| 145 |
-
"classe": "comp.sys.ibm.pc.hardware",
|
| 146 |
-
"percent": "0.17852880"
|
| 147 |
-
},
|
| 148 |
-
{
|
| 149 |
-
"classe": "comp.sys.mac.hardware",
|
| 150 |
-
"percent": "0.24781623"
|
| 151 |
-
},
|
| 152 |
-
{
|
| 153 |
-
"classe": "comp.windows.x",
|
| 154 |
-
"percent": "3.20503265"
|
| 155 |
-
},
|
| 156 |
-
{
|
| 157 |
-
"classe": "misc.forsale",
|
| 158 |
-
"percent": "0.16137564"
|
| 159 |
-
},
|
| 160 |
-
{
|
| 161 |
-
"classe": "rec.autos",
|
| 162 |
-
"percent": "0.23865439"
|
| 163 |
-
},
|
| 164 |
-
{
|
| 165 |
-
"classe": "rec.motorcycles",
|
| 166 |
-
"percent": "0.35177895"
|
| 167 |
-
},
|
| 168 |
-
{
|
| 169 |
-
"classe": "rec.sport.baseball",
|
| 170 |
-
"percent": "1.18482364"
|
| 171 |
-
},
|
| 172 |
-
{
|
| 173 |
-
"classe": "rec.sport.hockey",
|
| 174 |
-
"percent": "0.21046386"
|
| 175 |
-
},
|
| 176 |
-
{
|
| 177 |
-
"classe": "sci.crypt",
|
| 178 |
-
"percent": "4.29985709"
|
| 179 |
-
},
|
| 180 |
-
{
|
| 181 |
-
"classe": "sci.electronics",
|
| 182 |
-
"percent": "2.09880602"
|
| 183 |
-
},
|
| 184 |
-
{
|
| 185 |
-
"classe": "sci.med",
|
| 186 |
-
"percent": "19.70048994"
|
| 187 |
-
},
|
| 188 |
-
{
|
| 189 |
-
"classe": "sci.space",
|
| 190 |
-
"percent": "5.71478717"
|
| 191 |
-
},
|
| 192 |
-
{
|
| 193 |
-
"classe": "soc.religion.christian",
|
| 194 |
-
"percent": "11.07885465"
|
| 195 |
-
},
|
| 196 |
-
{
|
| 197 |
-
"classe": "talk.politics.guns",
|
| 198 |
-
"percent": "1.57866161"
|
| 199 |
-
},
|
| 200 |
-
{
|
| 201 |
-
"classe": "talk.politics.mideast",
|
| 202 |
-
"percent": "1.79922581"
|
| 203 |
-
},
|
| 204 |
-
{
|
| 205 |
-
"classe": "talk.politics.misc",
|
| 206 |
-
"percent": "3.07453331"
|
| 207 |
-
},
|
| 208 |
-
{
|
| 209 |
-
"classe": "talk.religion.misc",
|
| 210 |
-
"percent": "17.71455258"
|
| 211 |
-
}
|
| 212 |
-
]
|
| 213 |
-
```
|
|
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
| 3 |
title: ngt-ai-platform
|
| 4 |
+
sdk: gradio
|
| 5 |
+
emoji: 🚀
|
| 6 |
colorFrom: blue
|
| 7 |
colorTo: purple
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# 🚀 NGT AI Platform
|
| 12 |
|
| 13 |
+
**NextGenTech AI Platform** è una suite modulare di Intelligenza Artificiale progettata per dimostrare capacità avanzate in ambito **NLP** (Natural Language Processing) e **Computer Vision**.
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
La piattaforma è costruita con un'architettura ibrida che integra modelli **TensorFlow/Keras** (Legacy) e **PyTorch/Transformers** (NextGenTech), il tutto esposto tramite un'interfaccia web interattiva basata su **Gradio**.
|
| 16 |
|
| 17 |
+
  
|
| 18 |
|
| 19 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
## 🧩 Moduli Disponibili
|
| 22 |
|
| 23 |
+
### 1. 🧩 BPO Intelligent Dispatcher (NextGen)
|
| 24 |
+
Un sistema avanzato per l'analisi dei ticket di assistenza clienti (Business Process Outsourcing).
|
| 25 |
+
* **Tecnologia:** DistilBERT (Fine-tuned) + spaCy (NER) + Custom Logic.
|
| 26 |
+
* **Funzionalità:**
|
| 27 |
+
* **Intent Classification:** Riconosce automaticamente se il ticket riguarda *Amministrazione*, *Supporto Tecnico* o *Rischio Churn*.
|
| 28 |
+
* **Smart Urgency:** Calcola la priorità basandosi sulla gravità del problema e sul tono del cliente.
|
| 29 |
+
* **Hybrid NER:** Estrae dati strutturati (Codici Cliente, Fatture, Email) usando un motore ibrido AI + Regex Contestuale.
|
| 30 |
+
* **Visualizzazione:** Rendering HTML dinamico delle entità estratte.
|
| 31 |
|
| 32 |
+
### 2. 🩻 Healthcare Diagnostics (Computer Vision)
|
| 33 |
+
Moduli verticali per l'analisi di immagini mediche.
|
| 34 |
+
* **Chest X-Ray:** Classificazione di radiografie toraciche per individuare: *Polmonite*, *Tubercolosi* o *No Polmonite*, *No Tubercolosi*.
|
| 35 |
+
* **Diabetic Retinopathy:** Analisi del fondo oculare per rilevare segni di retinopatia diabetica.
|
| 36 |
|
| 37 |
+
### 3. 📰 Legacy NLP Stack
|
| 38 |
+
Moduli classici di analisi testuale.
|
| 39 |
+
* **Topic Classification:** Classificazione multiclasse su 20 categorie di news (Dataset 20 Newsgroups).
|
| 40 |
+
* **Sentiment Analysis:** Analisi binaria (Positivo/Negativo) del tono del testo.
|
| 41 |
|
| 42 |
+
---
|
| 43 |
|
| 44 |
+
## 🛠️ Installazione
|
|
|
|
|
|
|
| 45 |
|
| 46 |
+
Il progetto richiede **Python 3.10**. Si consiglia l'uso di un virtual environment.
|
| 47 |
|
| 48 |
+
### 1. Clona il repository
|
| 49 |
```bash
|
| 50 |
+
git clone git@github.com:gaeparente/ngt-ai-platform.git
|
| 51 |
+
cd ngt-ai-platform
|
| 52 |
```
|
| 53 |
|
| 54 |
+
### 2. Setup dell'ambiente virtuale
|
|
|
|
| 55 |
```bash
|
| 56 |
+
python -m venv .venv
|
| 57 |
+
source .venv/bin/activate # Su Linux/Mac
|
| 58 |
+
# .venv\Scripts\activate # Su Windows
|
| 59 |
```
|
| 60 |
|
| 61 |
+
### 3. Installazione Dipendenze
|
| 62 |
+
|
| 63 |
+
Il file requirements.txt è ottimizzato per installare le versioni CPU di PyTorch per risparmiare spazio.
|
| 64 |
|
| 65 |
```bash
|
| 66 |
+
pip install -r requirements.txt
|
| 67 |
```
|
| 68 |
+
Nota: Il sistema scaricherà automaticamente anche il modello linguistico italiano per spaCy (it_core_news_lg).
|
| 69 |
|
| 70 |
+
### 📂 Struttura Cartelle e Modelli
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
+
Affinché la piattaforma funzioni, è necessario posizionare i modelli addestrati nella cartella corretta. Assicurati che la struttura sia la seguente:
|
| 73 |
|
| 74 |
+
ngt-ai-platform/
|
| 75 |
+
├── app.py # Entry point dell'applicazione
|
| 76 |
+
├── requirements.txt # Dipendenze
|
| 77 |
+
├── modules/ # Logica di business
|
| 78 |
+
└── data/
|
| 79 |
+
├── model/ # CARTELLA MODELLI (Non versionata)
|
| 80 |
+
│ ├── bpo_bert_model/ # Cartella del modello BERT addestrato
|
| 81 |
+
│ └── ... # modelli addestrati da noi
|
| 82 |
+
├── gallery/ # Immagini di esempio per la Demo
|
| 83 |
+
└── tokenizer/ # tokenizer per la BinaryClassification e MultiClassification
|
| 84 |
|
| 85 |
+
### 🚀 Avvio Piattaforma
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
+
Una volta installato tutto, avvia l'interfaccia web con:
|
| 88 |
|
| 89 |
+
```bash
|
| 90 |
+
python app.py
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
```
|
| 92 |
+
L'applicazione sarà accessibile localmente all'indirizzo: 👉 http://127.0.0.1:7860
|
| 93 |
|
| 94 |
+
### 📄 License
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
+
Distributed under the Apache 2.0 License.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1,43 +1,10 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
import cv2
|
| 3 |
-
import os
|
| 4 |
-
|
| 5 |
from modules.binary_classification import binary_classification as binary
|
| 6 |
from modules.image_classification import image_classification as image
|
| 7 |
from modules.multilabel_classification import multi_classification as multi
|
| 8 |
from modules.retina import predict_diabetic_retinopathy as retina_detector
|
| 9 |
-
|
| 10 |
-
# -------------------------------------------------------------
|
| 11 |
-
|
| 12 |
-
def binary_classification(text):
|
| 13 |
-
if text.strip():
|
| 14 |
-
return binary(text)
|
| 15 |
-
raise gr.Error('Il testo è obbligatorio!')
|
| 16 |
-
|
| 17 |
-
def multi_classification(text):
|
| 18 |
-
if text.strip():
|
| 19 |
-
try:
|
| 20 |
-
return multi(text)
|
| 21 |
-
except Exception as e:
|
| 22 |
-
raise gr.Error(f'Errore nel modello: {str(e)}')
|
| 23 |
-
raise gr.Error('Il testo è obbligatorio!')
|
| 24 |
-
|
| 25 |
-
def file_change(file):
|
| 26 |
-
if isinstance(file, list):
|
| 27 |
-
file = file[0]
|
| 28 |
-
if file:
|
| 29 |
-
return cv2.imread(file)
|
| 30 |
-
return None
|
| 31 |
-
|
| 32 |
-
def image_classification(img):
|
| 33 |
-
if img is not None:
|
| 34 |
-
return image(img)
|
| 35 |
-
raise gr.Error('L\'immagine è obbligatoria!')
|
| 36 |
-
|
| 37 |
-
def retina_classification(retina):
|
| 38 |
-
if retina is not None:
|
| 39 |
-
return retina_detector(retina)
|
| 40 |
-
raise gr.Error('L\'immagine è obbligatoria!')
|
| 41 |
|
| 42 |
# --- CONFIGURAZIONE TEMA ---
|
| 43 |
theme = gr.themes.Soft(
|
|
@@ -61,12 +28,10 @@ custom_css = """
|
|
| 61 |
padding-bottom: 20px;
|
| 62 |
border-bottom: 1px solid #e2e8f0;
|
| 63 |
}
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
margin-bottom: 4px !important;
|
| 67 |
-
object-fit: contain;
|
| 68 |
}
|
| 69 |
-
|
| 70 |
.header-text-col h1 {
|
| 71 |
font-family: 'Inter', sans-serif !important;
|
| 72 |
font-weight: 900 !important;
|
|
@@ -79,221 +44,275 @@ custom_css = """
|
|
| 79 |
padding-bottom: 0 !important;
|
| 80 |
line-height: 1.0 !important;
|
| 81 |
}
|
| 82 |
-
|
| 83 |
.header-text-col .subheader {
|
| 84 |
-
text-align: left !important;
|
| 85 |
-
|
| 86 |
-
font-size: 1.1em;
|
| 87 |
-
font-weight: 500;
|
| 88 |
-
margin-top: 0 !important;
|
| 89 |
-
padding-top: 0 !important;
|
| 90 |
}
|
| 91 |
|
| 92 |
/* --- 2. CUSTOM TABS STYLE (DESKTOP) --- */
|
| 93 |
-
.tabs > .tab-nav {
|
| 94 |
-
border-bottom: none !important;
|
| 95 |
-
gap: 8px !important;
|
| 96 |
-
margin-bottom: 15px !important;
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
.tabs > .tab-nav > button {
|
| 100 |
-
border: 1px solid #e5e7eb !important;
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
color: #475569 !important;
|
| 104 |
-
font-weight: 600 !important;
|
| 105 |
-
transition: all 0.2s ease-in-out;
|
| 106 |
-
padding: 6px 16px !important;
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
.tabs > .tab-nav > button:hover {
|
| 110 |
-
background-color: #f1f5f9 !important;
|
| 111 |
-
transform: translateY(-1px);
|
| 112 |
}
|
| 113 |
-
|
| 114 |
.tabs > .tab-nav > button.selected {
|
| 115 |
background: linear-gradient(135deg, #8B5CF6 0%, #D65DB1 100%) !important;
|
| 116 |
-
color: white !important;
|
| 117 |
-
border: 1px solid transparent !important;
|
| 118 |
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3) !important;
|
| 119 |
}
|
| 120 |
|
| 121 |
-
/* --- 3.
|
| 122 |
button.primary {
|
| 123 |
background: linear-gradient(135deg, #8B5CF6 0%, #D65DB1 100%) !important;
|
| 124 |
-
border: none !important;
|
| 125 |
-
color: white !important;
|
| 126 |
-
transition: filter 0.2s;
|
| 127 |
-
}
|
| 128 |
-
button.primary:hover {
|
| 129 |
-
filter: brightness(1.1);
|
| 130 |
-
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.4);
|
| 131 |
}
|
|
|
|
| 132 |
|
| 133 |
-
|
| 134 |
-
.fixed-height {
|
| 135 |
-
height:
|
| 136 |
-
overflow: hidden !important;
|
| 137 |
}
|
| 138 |
-
.fixed-height
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
}
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
}
|
| 149 |
|
| 150 |
-
/* --- 4. MOBILE RESPONSIVE
|
| 151 |
@media (max-width: 768px) {
|
| 152 |
-
|
| 153 |
-
.header-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
gap: 10px !important;
|
| 158 |
-
}
|
| 159 |
-
.header-text-col h1 {
|
| 160 |
-
text-align: center !important;
|
| 161 |
-
font-size: 1.8em !important;
|
| 162 |
-
}
|
| 163 |
-
.header-text-col .subheader {
|
| 164 |
-
text-align: center !important;
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
/* Layout Moduli Stack */
|
| 168 |
-
.responsive-row {
|
| 169 |
-
flex-direction: column !important;
|
| 170 |
-
display: flex !important;
|
| 171 |
-
}
|
| 172 |
-
.responsive-row > * {
|
| 173 |
-
width: 100% !important;
|
| 174 |
-
min-width: 100% !important;
|
| 175 |
-
margin-bottom: 15px !important;
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
/* --- NUOVA GESTIONE TAB MOBILE --- */
|
| 179 |
-
.tabs > .tab-nav {
|
| 180 |
-
flex-wrap: wrap !important; /* Permette di andare a capo */
|
| 181 |
-
justify-content: center !important; /* Centra i bottoni */
|
| 182 |
-
gap: 6px !important; /* Spazio ridotto tra i bottoni */
|
| 183 |
-
}
|
| 184 |
|
|
|
|
| 185 |
.tabs > .tab-nav > button {
|
| 186 |
-
flex-grow: 1 !important;
|
| 187 |
-
|
| 188 |
-
font-size: 0.85rem !important; /* Testo leggermente più piccolo */
|
| 189 |
-
padding: 8px 10px !important; /* Padding ottimizzato per il tocco */
|
| 190 |
-
margin: 0 !important;
|
| 191 |
-
width: auto !important; /* Lascia decidere al flex-grow */
|
| 192 |
-
min-width: 45% !important; /* Assicura che al massimo ci siano 2 tab per riga */
|
| 193 |
}
|
| 194 |
}
|
| 195 |
-
|
| 196 |
footer {visibility: hidden}
|
| 197 |
"""
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
with gr.Blocks(theme=theme, css=custom_css, title="NGT AI Platform") as demo:
|
| 200 |
|
| 201 |
# --- HEADER ---
|
| 202 |
with gr.Row(elem_classes="header-row"):
|
| 203 |
with gr.Column(scale=0, min_width=80, elem_classes="logo-container"):
|
| 204 |
-
gr.Image(
|
| 205 |
-
value="data/icon.png",
|
| 206 |
-
show_label=False,
|
| 207 |
-
show_download_button=False,
|
| 208 |
-
show_share_button=False,
|
| 209 |
-
container=False,
|
| 210 |
-
show_fullscreen_button=False,
|
| 211 |
-
interactive=False,
|
| 212 |
-
height=80,
|
| 213 |
-
width=80
|
| 214 |
-
)
|
| 215 |
-
|
| 216 |
with gr.Column(scale=1, elem_classes="header-text-col"):
|
| 217 |
-
gr.Markdown("""
|
| 218 |
-
<h1>NGT AI Platform</h1>
|
| 219 |
-
<div class='subheader'>Advanced Machine Learning Solutions</div>
|
| 220 |
-
""")
|
| 221 |
|
| 222 |
-
# --- TAB 1:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
with gr.Tab("🩻 Chest Diagnosis"):
|
| 224 |
gr.Markdown("### 📥 Diagnostica Polmonare")
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
# Aggiunta classe "responsive-row" e rimosso equal_height=True (lo gestiamo col CSS se serve)
|
| 228 |
with gr.Row(elem_classes="responsive-row"):
|
| 229 |
with gr.Column(scale=1):
|
| 230 |
with gr.Accordion("📂 1. Seleziona da Gallery", open=True):
|
| 231 |
-
file_selected = gr.FileExplorer(
|
| 232 |
-
root_dir="data/gallery/xray",
|
| 233 |
-
file_count='single',
|
| 234 |
-
height=274 # Su desktop usa questo, su mobile il layout stackerà
|
| 235 |
-
)
|
| 236 |
with gr.Column(scale=1):
|
| 237 |
-
image_input = gr.Image(type="numpy",
|
| 238 |
-
|
| 239 |
with gr.Row():
|
| 240 |
with gr.Column():
|
| 241 |
analyze_btn_chest = gr.Button("🔍 Avvia Diagnosi Clinica", variant="primary", size="lg")
|
| 242 |
image_output = gr.Label(num_top_classes=2, label="Risultato Predittivo")
|
| 243 |
-
|
| 244 |
file_selected.change(file_change, inputs=file_selected, outputs=image_input)
|
| 245 |
analyze_btn_chest.click(image_classification, inputs=image_input, outputs=image_output)
|
| 246 |
|
| 247 |
-
# --- TAB
|
| 248 |
with gr.Tab("👁️ Diabetic Retinopathy"):
|
| 249 |
gr.Markdown("### 📥 Analisi Retinica")
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
# Aggiunta classe "responsive-row"
|
| 253 |
with gr.Row(elem_classes="responsive-row"):
|
| 254 |
with gr.Column(scale=1):
|
| 255 |
with gr.Accordion("📂 1. Seleziona da Gallery", open=True):
|
| 256 |
-
file_selected_dr = gr.FileExplorer(
|
| 257 |
-
root_dir="data/gallery/retinopaty",
|
| 258 |
-
file_count='single',
|
| 259 |
-
height=274
|
| 260 |
-
)
|
| 261 |
with gr.Column(scale=1):
|
| 262 |
-
image_input_dr = gr.Image(type="numpy",
|
| 263 |
-
|
| 264 |
with gr.Row():
|
| 265 |
with gr.Column():
|
| 266 |
analyze_btn_dr = gr.Button("🔍 Analizza Retina", variant="primary", size="lg")
|
| 267 |
with gr.Group():
|
| 268 |
output_dr_label = gr.Label(label="Diagnosi Principale")
|
| 269 |
output_dr_prob = gr.Label(label="Probabilità Patologia")
|
| 270 |
-
|
| 271 |
file_selected_dr.change(file_change, inputs=file_selected_dr, outputs=image_input_dr)
|
| 272 |
analyze_btn_dr.click(retina_classification, inputs=image_input_dr, outputs=[output_dr_label, output_dr_prob])
|
| 273 |
|
| 274 |
-
# --- TAB
|
| 275 |
with gr.Tab("📰 Topic Classification"):
|
| 276 |
gr.Markdown("### Analisi Argomenti del Testo")
|
| 277 |
-
with gr.Row():
|
|
|
|
| 278 |
with gr.Column():
|
| 279 |
multi_input = gr.Textbox(lines=5, placeholder="Incolla qui il testo...", label="Input")
|
| 280 |
analyze_btn_multi = gr.Button("🏷️ Classifica", variant="primary")
|
|
|
|
| 281 |
with gr.Column():
|
| 282 |
multi_output = gr.Label(num_top_classes=5, label="Top Categorie")
|
| 283 |
-
|
| 284 |
-
gr.Examples(examples=[["La NASA ha lanciato un nuovo satellite."], ["Il prezzo della GPU è sceso."]], inputs=multi_input)
|
| 285 |
analyze_btn_multi.click(multi_classification, inputs=multi_input, outputs=multi_output)
|
| 286 |
|
| 287 |
-
# --- TAB
|
| 288 |
with gr.Tab("😊 Sentiment Analysis"):
|
| 289 |
gr.Markdown("### Analisi del Sentiment")
|
| 290 |
-
with gr.Row():
|
|
|
|
| 291 |
with gr.Column():
|
| 292 |
binary_input = gr.Textbox(lines=3, placeholder="Scrivi una recensione...", label="Input")
|
| 293 |
analyze_btn_bin = gr.Button("⚖️ Analizza", variant="primary")
|
|
|
|
| 294 |
with gr.Column():
|
| 295 |
binary_output = gr.Label(label="Sentiment Score")
|
| 296 |
-
|
| 297 |
analyze_btn_bin.click(binary_classification, inputs=binary_input, outputs=binary_output)
|
| 298 |
|
| 299 |
if __name__ == "__main__":
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import cv2
|
|
|
|
|
|
|
| 3 |
from modules.binary_classification import binary_classification as binary
|
| 4 |
from modules.image_classification import image_classification as image
|
| 5 |
from modules.multilabel_classification import multi_classification as multi
|
| 6 |
from modules.retina import predict_diabetic_retinopathy as retina_detector
|
| 7 |
+
from modules.bpo_dispatcher import predict_bpo_ticket
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
# --- CONFIGURAZIONE TEMA ---
|
| 10 |
theme = gr.themes.Soft(
|
|
|
|
| 28 |
padding-bottom: 20px;
|
| 29 |
border-bottom: 1px solid #e2e8f0;
|
| 30 |
}
|
| 31 |
+
.h4-margin{
|
| 32 |
+
margin-left: 5px;
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
+
.logo-container img { margin-bottom: 4px !important; object-fit: contain; }
|
| 35 |
.header-text-col h1 {
|
| 36 |
font-family: 'Inter', sans-serif !important;
|
| 37 |
font-weight: 900 !important;
|
|
|
|
| 44 |
padding-bottom: 0 !important;
|
| 45 |
line-height: 1.0 !important;
|
| 46 |
}
|
|
|
|
| 47 |
.header-text-col .subheader {
|
| 48 |
+
text-align: left !important; color: #64748b; font-size: 1.1em; font-weight: 500;
|
| 49 |
+
margin-top: 0 !important; padding-top: 0 !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
/* --- 2. CUSTOM TABS STYLE (DESKTOP) --- */
|
| 53 |
+
.tabs > .tab-nav { border-bottom: none !important; gap: 8px !important; margin-bottom: 15px !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
.tabs > .tab-nav > button {
|
| 55 |
+
border: 1px solid #e5e7eb !important; border-radius: 10px !important;
|
| 56 |
+
background-color: white; color: #475569 !important; font-weight: 600 !important;
|
| 57 |
+
padding: 6px 16px !important; transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
+
.tabs > .tab-nav > button:hover { background-color: #f1f5f9 !important; transform: translateY(-1px); }
|
| 60 |
.tabs > .tab-nav > button.selected {
|
| 61 |
background: linear-gradient(135deg, #8B5CF6 0%, #D65DB1 100%) !important;
|
| 62 |
+
color: white !important; border: 1px solid transparent !important;
|
|
|
|
| 63 |
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3) !important;
|
| 64 |
}
|
| 65 |
|
| 66 |
+
/* --- 3. COMPONENTS --- */
|
| 67 |
button.primary {
|
| 68 |
background: linear-gradient(135deg, #8B5CF6 0%, #D65DB1 100%) !important;
|
| 69 |
+
border: none !important; color: white !important; transition: filter 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
+
button.primary:hover { filter: brightness(1.1); box-shadow: 0 4px 15px rgba(139, 92, 246, 0.4); }
|
| 72 |
|
| 73 |
+
.fixed-height { height: 380px !important; overflow: hidden !important; }
|
| 74 |
+
.fixed-height button, .fixed-height .image-container, .fixed-height .upload-container {
|
| 75 |
+
height: 100% !important; max_height: 100% !important; min_height: 100% !important;
|
|
|
|
| 76 |
}
|
| 77 |
+
.fixed-height img { object-fit: contain !important; max_height: 100% !important; }
|
| 78 |
+
|
| 79 |
+
/* Stile per la Model Card nel tab BPO */
|
| 80 |
+
.model-card {
|
| 81 |
+
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 15px;
|
| 82 |
+
font-size: 0.9em; color: #475569; margin-top: 10px;
|
| 83 |
}
|
| 84 |
+
|
| 85 |
+
.model-card strong{
|
| 86 |
+
color: #475569 !important
|
| 87 |
}
|
| 88 |
|
| 89 |
+
/* --- 4. MOBILE RESPONSIVE --- */
|
| 90 |
@media (max-width: 768px) {
|
| 91 |
+
.header-row { flex-direction: column !important; align-items: center !important; text-align: center !important; gap: 10px !important; }
|
| 92 |
+
.header-text-col h1 { text-align: center !important; font-size: 1.8em !important; }
|
| 93 |
+
.header-text-col .subheader { text-align: center !important; }
|
| 94 |
+
.responsive-row { flex-direction: column !important; display: flex !important; }
|
| 95 |
+
.responsive-row > * { width: 100% !important; min-width: 100% !important; margin-bottom: 15px !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
+
.tabs > .tab-nav { flex-wrap: wrap !important; justify-content: center !important; gap: 6px !important; }
|
| 98 |
.tabs > .tab-nav > button {
|
| 99 |
+
flex-grow: 1 !important; text-align: center !important; font-size: 0.85rem !important;
|
| 100 |
+
padding: 8px 10px !important; margin: 0 !important; width: auto !important; min-width: 45% !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
}
|
| 102 |
}
|
|
|
|
| 103 |
footer {visibility: hidden}
|
| 104 |
"""
|
| 105 |
|
| 106 |
+
def binary_classification(text):
|
| 107 |
+
if text.strip(): return binary(text)
|
| 108 |
+
raise gr.Error('Il testo è obbligatorio!')
|
| 109 |
+
|
| 110 |
+
def multi_classification(text):
|
| 111 |
+
if text.strip():
|
| 112 |
+
try: return multi(text)
|
| 113 |
+
except Exception as e: raise gr.Error(f'Errore nel modello: {str(e)}')
|
| 114 |
+
raise gr.Error('Il testo è obbligatorio!')
|
| 115 |
+
|
| 116 |
+
def file_change(file):
|
| 117 |
+
if isinstance(file, list): file = file[0]
|
| 118 |
+
if file: return cv2.imread(file)
|
| 119 |
+
return None
|
| 120 |
+
|
| 121 |
+
def image_classification(img):
|
| 122 |
+
if img is not None: return image(img)
|
| 123 |
+
raise gr.Error('L\'immagine è obbligatoria!')
|
| 124 |
+
|
| 125 |
+
def retina_classification(retina):
|
| 126 |
+
if retina is not None: return retina_detector(retina)
|
| 127 |
+
raise gr.Error('L\'immagine è obbligatoria!')
|
| 128 |
+
|
| 129 |
+
def render_ner_html(entities):
|
| 130 |
+
"""
|
| 131 |
+
Trasforma la lista [('testo', 'LABEL'), ('testo', None)] in HTML puro.
|
| 132 |
+
"""
|
| 133 |
+
# Mappa colori HEX (più belli e moderni)
|
| 134 |
+
colors = {
|
| 135 |
+
"CODICE CLIENTE": "#3b82f6", # Blue 500
|
| 136 |
+
"N. FATTURA": "#f97316", # Orange 500
|
| 137 |
+
"COD. FORNITURA": "#d946ef", # Fuchsia 500
|
| 138 |
+
"EMAIL": "#ef4444", # Red 500
|
| 139 |
+
"TELEFONO": "#06b6d4", # Cyan 500
|
| 140 |
+
"PERSONA": "#22c55e", # Green 500
|
| 141 |
+
"AZIENDA": "#8b5cf6", # Violet 500
|
| 142 |
+
"LUOGO": "#64748b", # Slate 500
|
| 143 |
+
"POSSIBILE ID": "#a8a29e" # Stone 400
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
html = "<div style='line-height: 2.2; font-family: sans-serif; font-size: 16px; color: #334155; background-color: #1e293b'>"
|
| 147 |
+
|
| 148 |
+
for text, label in entities:
|
| 149 |
+
if label:
|
| 150 |
+
# Recupera colore o usa grigio di default
|
| 151 |
+
c = colors.get(label, "#1e293b")
|
| 152 |
+
|
| 153 |
+
# Crea lo "Chip" (Pillola colorata)
|
| 154 |
+
# - bg-color con opacità (c + '20')
|
| 155 |
+
# - border solido
|
| 156 |
+
# - label piccola in grassetto accanto al testo
|
| 157 |
+
html += f"""
|
| 158 |
+
<span style='background-color: {c}20; border: 1px solid {c}; border-radius: 6px; padding: 2px 6px; margin: 0 2px; white-space: nowrap;'>
|
| 159 |
+
<span style='font-size: 0.75em; font-weight: 700; color: {c}; text-transform: uppercase;'>{label}</span>
|
| 160 |
+
<span style='font-weight: 600; color: white; margin-left: 6px;'>{text}</span>
|
| 161 |
+
</span>
|
| 162 |
+
"""
|
| 163 |
+
else:
|
| 164 |
+
# Testo normale
|
| 165 |
+
html += text.replace("\n", "<br>") # Gestisce a capo
|
| 166 |
+
|
| 167 |
+
html += "</div>"
|
| 168 |
+
return html
|
| 169 |
+
|
| 170 |
+
def bpo_dispatch_logic(text):
|
| 171 |
+
"""
|
| 172 |
+
Funzione Ponte: Chiama il modulo AI e decide l'azione di business.
|
| 173 |
+
Restituisce un aggiornamento COMPLETO del componente NER per pulire la grafica.
|
| 174 |
+
"""
|
| 175 |
+
try:
|
| 176 |
+
# 1. Chiamata al modello reale
|
| 177 |
+
intent, urgency, entities = predict_bpo_ticket(text)
|
| 178 |
+
|
| 179 |
+
if intent is None:
|
| 180 |
+
raise gr.Error("Errore nel modello BPO. Verifica i log.")
|
| 181 |
+
|
| 182 |
+
# 2. Logica di Business
|
| 183 |
+
top_intent = max(intent, key=intent.get)
|
| 184 |
+
|
| 185 |
+
action = "Inoltro generico"
|
| 186 |
+
if top_intent == "Retention / Churn Risk":
|
| 187 |
+
action = "🚨 ALERT: Assegnazione coda 'Retention' + Chiamata Outbound"
|
| 188 |
+
elif top_intent == "Supporto Tecnico":
|
| 189 |
+
action = "🛠️ Apertura Ticket JIRA (Livello 1) - Priorità Tecnica"
|
| 190 |
+
elif top_intent == "Amministrazione / Billing":
|
| 191 |
+
action = "💰 Verifica insoluti su SAP + Inoltro Backoffice Amm.vo"
|
| 192 |
+
|
| 193 |
+
html_output = render_ner_html(entities)
|
| 194 |
+
|
| 195 |
+
return intent, urgency, action, html_output
|
| 196 |
+
|
| 197 |
+
except Exception as e:
|
| 198 |
+
raise gr.Error(f"Errore nell'analisi: {str(e)}")
|
| 199 |
+
|
| 200 |
with gr.Blocks(theme=theme, css=custom_css, title="NGT AI Platform") as demo:
|
| 201 |
|
| 202 |
# --- HEADER ---
|
| 203 |
with gr.Row(elem_classes="header-row"):
|
| 204 |
with gr.Column(scale=0, min_width=80, elem_classes="logo-container"):
|
| 205 |
+
gr.Image(value="data/icon.png", show_label=False, show_download_button=False, show_share_button=False, container=False, show_fullscreen_button=False, interactive=False, height=80, width=80)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
with gr.Column(scale=1, elem_classes="header-text-col"):
|
| 207 |
+
gr.Markdown("""<h1>AI Platform</h1><div class='subheader'>Advanced Machine Learning Solutions</div>""")
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
+
# --- TAB 1: BPO INTELLIGENT DISPATCHER ---
|
| 210 |
+
with gr.Tab("🧩 BPO Dispatcher"):
|
| 211 |
+
gr.Markdown("### Intelligent Ticket Routing & NER")
|
| 212 |
+
gr.Markdown("Sistema proprietario per l'analisi automatica dei ticket di assistenza. Il modello identifica l'intento, l'urgenza e i dati sensibili del cliente.")
|
| 213 |
+
|
| 214 |
+
with gr.Row(elem_classes="responsive-row"):
|
| 215 |
+
# INPUT
|
| 216 |
+
with gr.Column(scale=1):
|
| 217 |
+
bpo_input = gr.Textbox(lines=8, placeholder="Incolla qui il contenuto della mail o del ticket...", label="Contenuto Ticket / Email")
|
| 218 |
+
analyze_btn_bpo = gr.Button("⚡ Analizza Richiesta", variant="primary")
|
| 219 |
+
gr.HTML("""
|
| 220 |
+
<div class='model-card'>
|
| 221 |
+
<strong>🛠️ Model Architecture:</strong> NGT-BERT-Custom (DistilBERT)<br>
|
| 222 |
+
<strong>📚 Training Data:</strong> Synthetic BPO Dataset (2025)<br>
|
| 223 |
+
<strong>🎯 Tasks:</strong> Intent Classification (Multi-class), Entity Extraction (NER)
|
| 224 |
+
</div>
|
| 225 |
+
""")
|
| 226 |
+
|
| 227 |
+
# OUTPUT
|
| 228 |
+
with gr.Column(scale=1):
|
| 229 |
+
with gr.Group():
|
| 230 |
+
gr.Markdown("#### 📋 Analisi Processata", elem_classes="h4-margin")
|
| 231 |
+
bpo_intent_output = gr.Label(num_top_classes=3, label="Intento Rilevato")
|
| 232 |
+
with gr.Row():
|
| 233 |
+
bpo_urgency_output = gr.Textbox(label="Livello Urgenza", scale=1)
|
| 234 |
+
bpo_action_output = gr.Textbox(label="Azione Consigliata (Auto)", scale=1)
|
| 235 |
+
|
| 236 |
+
gr.Markdown("#### 🔍 Dati Estratti (NER)", elem_classes="h4-margin")
|
| 237 |
+
bpo_ner_output = gr.HTML(label="Visualizzazione Entità")
|
| 238 |
+
|
| 239 |
+
gr.Examples(
|
| 240 |
+
examples=[
|
| 241 |
+
["Buongiorno, vi scrivo perché la fattura n. 99283 del mese scorso è sbagliata. Non ho consumato così tanto. Il mio codice cliente è 4599201. Attendo rettifica urgente."],
|
| 242 |
+
["Salve, il servizio non funziona da ieri. Mi dà errore 504 sul router. Risolvete subito per favore!"],
|
| 243 |
+
["Vorrei disdire il contratto con decorrenza immediata se non mi risolvete il problema."]
|
| 244 |
+
],
|
| 245 |
+
inputs=bpo_input
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
analyze_btn_bpo.click(
|
| 249 |
+
bpo_dispatch_logic,
|
| 250 |
+
inputs=bpo_input,
|
| 251 |
+
outputs=[bpo_intent_output, bpo_urgency_output, bpo_action_output, bpo_ner_output]
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
# --- TAB 2: Chest X-Ray ---
|
| 255 |
with gr.Tab("🩻 Chest Diagnosis"):
|
| 256 |
gr.Markdown("### 📥 Diagnostica Polmonare")
|
| 257 |
+
# INPUT
|
|
|
|
|
|
|
| 258 |
with gr.Row(elem_classes="responsive-row"):
|
| 259 |
with gr.Column(scale=1):
|
| 260 |
with gr.Accordion("📂 1. Seleziona da Gallery", open=True):
|
| 261 |
+
file_selected = gr.FileExplorer(root_dir="data/gallery/xray", file_count='single', elem_classes=["fixed-height"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
with gr.Column(scale=1):
|
| 263 |
+
image_input = gr.Image(type="numpy", label="2. Visualizzazione", elem_classes=["fixed-height"])
|
| 264 |
+
# OUTPUT
|
| 265 |
with gr.Row():
|
| 266 |
with gr.Column():
|
| 267 |
analyze_btn_chest = gr.Button("🔍 Avvia Diagnosi Clinica", variant="primary", size="lg")
|
| 268 |
image_output = gr.Label(num_top_classes=2, label="Risultato Predittivo")
|
|
|
|
| 269 |
file_selected.change(file_change, inputs=file_selected, outputs=image_input)
|
| 270 |
analyze_btn_chest.click(image_classification, inputs=image_input, outputs=image_output)
|
| 271 |
|
| 272 |
+
# --- TAB 3: Diabetic Retinopathy ---
|
| 273 |
with gr.Tab("👁️ Diabetic Retinopathy"):
|
| 274 |
gr.Markdown("### 📥 Analisi Retinica")
|
| 275 |
+
# INPUT
|
|
|
|
|
|
|
| 276 |
with gr.Row(elem_classes="responsive-row"):
|
| 277 |
with gr.Column(scale=1):
|
| 278 |
with gr.Accordion("📂 1. Seleziona da Gallery", open=True):
|
| 279 |
+
file_selected_dr = gr.FileExplorer(root_dir="data/gallery/retinopaty", file_count='single', elem_classes=["fixed-height"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
with gr.Column(scale=1):
|
| 281 |
+
image_input_dr = gr.Image(type="numpy", label="2. Visualizzazione", elem_classes=["fixed-height"])
|
| 282 |
+
# OUTPUT
|
| 283 |
with gr.Row():
|
| 284 |
with gr.Column():
|
| 285 |
analyze_btn_dr = gr.Button("🔍 Analizza Retina", variant="primary", size="lg")
|
| 286 |
with gr.Group():
|
| 287 |
output_dr_label = gr.Label(label="Diagnosi Principale")
|
| 288 |
output_dr_prob = gr.Label(label="Probabilità Patologia")
|
|
|
|
| 289 |
file_selected_dr.change(file_change, inputs=file_selected_dr, outputs=image_input_dr)
|
| 290 |
analyze_btn_dr.click(retina_classification, inputs=image_input_dr, outputs=[output_dr_label, output_dr_prob])
|
| 291 |
|
| 292 |
+
# --- TAB 4: Review Classification ---
|
| 293 |
with gr.Tab("📰 Topic Classification"):
|
| 294 |
gr.Markdown("### Analisi Argomenti del Testo")
|
| 295 |
+
with gr.Row(elem_classes="responsive-row"):
|
| 296 |
+
# INPUT
|
| 297 |
with gr.Column():
|
| 298 |
multi_input = gr.Textbox(lines=5, placeholder="Incolla qui il testo...", label="Input")
|
| 299 |
analyze_btn_multi = gr.Button("🏷️ Classifica", variant="primary")
|
| 300 |
+
# OUTPUT
|
| 301 |
with gr.Column():
|
| 302 |
multi_output = gr.Label(num_top_classes=5, label="Top Categorie")
|
|
|
|
|
|
|
| 303 |
analyze_btn_multi.click(multi_classification, inputs=multi_input, outputs=multi_output)
|
| 304 |
|
| 305 |
+
# --- TAB 5: Sentiment Analysis ---
|
| 306 |
with gr.Tab("😊 Sentiment Analysis"):
|
| 307 |
gr.Markdown("### Analisi del Sentiment")
|
| 308 |
+
with gr.Row(elem_classes="responsive-row"):
|
| 309 |
+
# INPUT
|
| 310 |
with gr.Column():
|
| 311 |
binary_input = gr.Textbox(lines=3, placeholder="Scrivi una recensione...", label="Input")
|
| 312 |
analyze_btn_bin = gr.Button("⚖️ Analizza", variant="primary")
|
| 313 |
+
# OUTPUT
|
| 314 |
with gr.Column():
|
| 315 |
binary_output = gr.Label(label="Sentiment Score")
|
|
|
|
| 316 |
analyze_btn_bin.click(binary_classification, inputs=binary_input, outputs=binary_output)
|
| 317 |
|
| 318 |
if __name__ == "__main__":
|
data/model/bpo_bert_model/config.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"activation": "gelu",
|
| 3 |
+
"architectures": [
|
| 4 |
+
"DistilBertForSequenceClassification"
|
| 5 |
+
],
|
| 6 |
+
"attention_dropout": 0.1,
|
| 7 |
+
"bos_token_id": null,
|
| 8 |
+
"dim": 768,
|
| 9 |
+
"dropout": 0.1,
|
| 10 |
+
"dtype": "float32",
|
| 11 |
+
"eos_token_id": null,
|
| 12 |
+
"hidden_dim": 3072,
|
| 13 |
+
"id2label": {
|
| 14 |
+
"0": "LABEL_0",
|
| 15 |
+
"1": "LABEL_1",
|
| 16 |
+
"2": "LABEL_2"
|
| 17 |
+
},
|
| 18 |
+
"initializer_range": 0.02,
|
| 19 |
+
"label2id": {
|
| 20 |
+
"LABEL_0": 0,
|
| 21 |
+
"LABEL_1": 1,
|
| 22 |
+
"LABEL_2": 2
|
| 23 |
+
},
|
| 24 |
+
"max_position_embeddings": 512,
|
| 25 |
+
"model_type": "distilbert",
|
| 26 |
+
"n_heads": 12,
|
| 27 |
+
"n_layers": 6,
|
| 28 |
+
"output_past": true,
|
| 29 |
+
"pad_token_id": 0,
|
| 30 |
+
"problem_type": "single_label_classification",
|
| 31 |
+
"qa_dropout": 0.1,
|
| 32 |
+
"seq_classif_dropout": 0.2,
|
| 33 |
+
"sinusoidal_pos_embds": false,
|
| 34 |
+
"tie_weights_": true,
|
| 35 |
+
"tie_word_embeddings": true,
|
| 36 |
+
"transformers_version": "5.0.0",
|
| 37 |
+
"use_cache": false,
|
| 38 |
+
"vocab_size": 119547
|
| 39 |
+
}
|
data/model/bpo_bert_model/tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/model/bpo_bert_model/tokenizer_config.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"backend": "tokenizers",
|
| 3 |
+
"cls_token": "[CLS]",
|
| 4 |
+
"do_lower_case": false,
|
| 5 |
+
"is_local": false,
|
| 6 |
+
"mask_token": "[MASK]",
|
| 7 |
+
"model_max_length": 512,
|
| 8 |
+
"pad_token": "[PAD]",
|
| 9 |
+
"sep_token": "[SEP]",
|
| 10 |
+
"strip_accents": null,
|
| 11 |
+
"tokenize_chinese_chars": true,
|
| 12 |
+
"tokenizer_class": "DistilBertTokenizer",
|
| 13 |
+
"unk_token": "[UNK]"
|
| 14 |
+
}
|
modules/bpo_dispatcher.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from transformers import DistilBertTokenizerFast, DistilBertForSequenceClassification
|
| 3 |
+
import spacy
|
| 4 |
+
import re
|
| 5 |
+
import os
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
from modules.binary_classification import binary_classification
|
| 10 |
+
except ImportError:
|
| 11 |
+
print("⚠️ Modulo sentiment non trovato. L'urgenza sarà basata solo sulle keyword.")
|
| 12 |
+
binary_classification = None
|
| 13 |
+
|
| 14 |
+
LABELS_MAP = {
|
| 15 |
+
0: "Amministrazione / Billing",
|
| 16 |
+
1: "Supporto Tecnico",
|
| 17 |
+
2: "Retention / Churn Risk"
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
class BPODispatcher:
|
| 21 |
+
def __init__(self, model_path="data/model/bpo_bert_model"):
|
| 22 |
+
self.model = None
|
| 23 |
+
self.tokenizer = None
|
| 24 |
+
self.nlp = None
|
| 25 |
+
self.device = "cpu"
|
| 26 |
+
|
| 27 |
+
# 1. BERT
|
| 28 |
+
if os.path.exists(model_path):
|
| 29 |
+
try:
|
| 30 |
+
self.tokenizer = DistilBertTokenizerFast.from_pretrained(model_path)
|
| 31 |
+
self.model = DistilBertForSequenceClassification.from_pretrained(model_path)
|
| 32 |
+
self.model.to(self.device)
|
| 33 |
+
self.model.eval()
|
| 34 |
+
print("✅ Modello BERT caricato.")
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"❌ Errore BERT: {e}")
|
| 37 |
+
|
| 38 |
+
# 2. spaCy
|
| 39 |
+
try:
|
| 40 |
+
self.nlp = spacy.load("it_core_news_lg")
|
| 41 |
+
print("✅ spaCy caricato.")
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"❌ Errore spaCy: {e}")
|
| 44 |
+
|
| 45 |
+
def _extract_smart_entities(self, text):
|
| 46 |
+
entities = []
|
| 47 |
+
occupied_spans = [] # Tiene traccia delle zone di testo già etichettate
|
| 48 |
+
|
| 49 |
+
def is_overlapping(start, end):
|
| 50 |
+
"""Controlla se la posizione è già occupata"""
|
| 51 |
+
for occ_start, occ_end in occupied_spans:
|
| 52 |
+
if (start < occ_end) and (end > occ_start):
|
| 53 |
+
return True
|
| 54 |
+
return False
|
| 55 |
+
|
| 56 |
+
def add_entity(text_val, label, start, end):
|
| 57 |
+
"""Aggiunge l'entità solo se non si sovrappone e la registra"""
|
| 58 |
+
if not is_overlapping(start, end):
|
| 59 |
+
entities.append((text_val, label))
|
| 60 |
+
occupied_spans.append((start, end))
|
| 61 |
+
|
| 62 |
+
# --- FASE 1: REGEX ALTA PRIORITÀ (Dati Strutturati) ---
|
| 63 |
+
|
| 64 |
+
# A. EMAIL
|
| 65 |
+
for m in re.finditer(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', text):
|
| 66 |
+
add_entity(m.group(), "EMAIL", m.start(), m.end())
|
| 67 |
+
|
| 68 |
+
# B. TELEFONO (Mobile e Fisso Italiano)
|
| 69 |
+
# Cerca pattern tipo 3xx... o 0x... con spazi opzionali
|
| 70 |
+
for m in re.finditer(r'\b(?:3\d{2}|0\d{1,4})[\s.-]?\d{6,10}\b', text):
|
| 71 |
+
add_entity(m.group(), "TELEFONO", m.start(), m.end())
|
| 72 |
+
|
| 73 |
+
# C. NUMERI CONTESTUALI (Fatture, Clienti, Forniture)
|
| 74 |
+
# Regex migliorata: Accetta anche alfanumerici per codici cliente
|
| 75 |
+
# Pattern: Parola che inizia o finisce con cifra, lunga 4-15 chars
|
| 76 |
+
candidates = re.finditer(r'\b(?=[A-Za-z0-9]*\d)[A-Za-z0-9]{4,15}\b', text)
|
| 77 |
+
|
| 78 |
+
window_size = 35
|
| 79 |
+
|
| 80 |
+
for match in candidates:
|
| 81 |
+
val = match.group()
|
| 82 |
+
start, end = match.span()
|
| 83 |
+
|
| 84 |
+
# Se è già un telefono o mail, salta
|
| 85 |
+
if is_overlapping(start, end): continue
|
| 86 |
+
|
| 87 |
+
context = text[max(0, start - window_size):start].lower()
|
| 88 |
+
|
| 89 |
+
# 1. Fatture
|
| 90 |
+
if any(w in context for w in ["fattura", "bolletta", "nota", "nr.", "n."]):
|
| 91 |
+
# Verifica extra: le fatture solitamente sono solo numeri o hanno /
|
| 92 |
+
if val.isdigit() or '/' in val:
|
| 93 |
+
add_entity(val, "N. FATTURA", start, end)
|
| 94 |
+
continue
|
| 95 |
+
|
| 96 |
+
# 2. Forniture (POD/PDR/Luce/Gas)
|
| 97 |
+
if any(w in context for w in ["luce", "gas", "fornitura", "pod", "pdr", "contatore"]):
|
| 98 |
+
add_entity(val, "COD. FORNITURA", start, end)
|
| 99 |
+
continue
|
| 100 |
+
|
| 101 |
+
# 3. Codici Cliente (più generico, accetta alfanumerici)
|
| 102 |
+
if any(w in context for w in ["cliente", "codice", "utenza", "pratica", "id"]):
|
| 103 |
+
add_entity(val, "CODICE CLIENTE", start, end)
|
| 104 |
+
continue
|
| 105 |
+
|
| 106 |
+
# --- FASE 2: SPACY BASSA PRIORITÀ (Entità Semantiche) ---
|
| 107 |
+
if self.nlp:
|
| 108 |
+
doc = self.nlp(text)
|
| 109 |
+
for ent in doc.ents:
|
| 110 |
+
# VALIDAZIONE ANTI-ALLUCINAZIONE
|
| 111 |
+
# Regola: Una PERSONA non può contenere cifre
|
| 112 |
+
if ent.label_ == "PER":
|
| 113 |
+
if any(char.isdigit() for char in ent.text):
|
| 114 |
+
continue # Scarta "25458958" classificato come Persona
|
| 115 |
+
if len(ent.text) < 3:
|
| 116 |
+
continue # Scarta nomi troppo corti
|
| 117 |
+
|
| 118 |
+
add_entity(ent.text, "PERSONA", ent.start_char, ent.end_char)
|
| 119 |
+
|
| 120 |
+
elif ent.label_ == "ORG":
|
| 121 |
+
add_entity(ent.text, "AZIENDA", ent.start_char, ent.end_char)
|
| 122 |
+
return entities
|
| 123 |
+
|
| 124 |
+
def _calculate_smart_urgency(self, text, intent_label):
|
| 125 |
+
"""
|
| 126 |
+
MATRICE DI URGENZA (Intent + Sentiment)
|
| 127 |
+
Combina la gravità del problema con lo stato d'animo del cliente.
|
| 128 |
+
"""
|
| 129 |
+
urgency = "Bassa"
|
| 130 |
+
|
| 131 |
+
# 1. Analisi Sentiment (Se disponibile)
|
| 132 |
+
sentiment_score_neg = 0.0
|
| 133 |
+
if binary_classification:
|
| 134 |
+
try:
|
| 135 |
+
# binary_classification restituisce un dict {'POSITIVE': 0.x, 'NEGATIVE': 0.y}
|
| 136 |
+
sent_result = binary_classification(text)
|
| 137 |
+
sentiment_score_neg = sent_result.get('NEGATIVE', 0.0)
|
| 138 |
+
except Exception:
|
| 139 |
+
sentiment_score_neg = 0.5 # Fallback neutro
|
| 140 |
+
|
| 141 |
+
# 2. Matrice Decisionale
|
| 142 |
+
|
| 143 |
+
# CASO A: CHURN (Disdetta) -> Sempre Critico
|
| 144 |
+
if intent_label == "Retention / Churn Risk":
|
| 145 |
+
return "CRITICA (Rischio Abbandono)"
|
| 146 |
+
|
| 147 |
+
# CASO B: SUPPORTO TECNICO
|
| 148 |
+
elif intent_label == "Supporto Tecnico":
|
| 149 |
+
if sentiment_score_neg > 0.9: # Molto arrabbiato
|
| 150 |
+
return "ALTA (Tecnico + Cliente Furioso)"
|
| 151 |
+
elif "fermo" in text.lower() or "blocco" in text.lower():
|
| 152 |
+
return "ALTA (Fermo Servizio)"
|
| 153 |
+
else:
|
| 154 |
+
return "MEDIA (Guasto Standard)"
|
| 155 |
+
|
| 156 |
+
# CASO C: AMMINISTRAZIONE
|
| 157 |
+
elif intent_label == "Amministrazione / Billing":
|
| 158 |
+
if sentiment_score_neg > 0.95: # Furioso per i soldi
|
| 159 |
+
return "ALTA (Contestazione Aggressiva)"
|
| 160 |
+
elif "scadenza" in text.lower() or "stacco" in text.lower():
|
| 161 |
+
return "MEDIA (Rischio Amministrativo)"
|
| 162 |
+
else:
|
| 163 |
+
return "BASSA (Info / Richiesta)"
|
| 164 |
+
|
| 165 |
+
return urgency
|
| 166 |
+
|
| 167 |
+
def predict(self, text):
|
| 168 |
+
if self.model is None: return None, "Errore", []
|
| 169 |
+
if not text.strip(): return None, "Vuoto", []
|
| 170 |
+
|
| 171 |
+
# 1. Intent Classification (BERT)
|
| 172 |
+
inputs = self.tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True)
|
| 173 |
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
| 174 |
+
with torch.no_grad():
|
| 175 |
+
outputs = self.model(**inputs)
|
| 176 |
+
probs = F.softmax(outputs.logits, dim=-1)
|
| 177 |
+
label_output = {LABELS_MAP[i]: float(probs[0][i]) for i in range(len(LABELS_MAP))}
|
| 178 |
+
|
| 179 |
+
# Prendi l'intento vincente
|
| 180 |
+
top_idx = torch.max(probs, dim=-1)[1].item()
|
| 181 |
+
predicted_label = LABELS_MAP[top_idx]
|
| 182 |
+
|
| 183 |
+
# 2. Urgenza Intelligente (AI + Sentiment + Rules)
|
| 184 |
+
urgency = self._calculate_smart_urgency(text, predicted_label)
|
| 185 |
+
|
| 186 |
+
# 3. NER Extraction
|
| 187 |
+
entities = self._extract_smart_entities(text)
|
| 188 |
+
|
| 189 |
+
return label_output, urgency, entities
|
| 190 |
+
|
| 191 |
+
dispatcher = BPODispatcher()
|
| 192 |
+
def predict_bpo_ticket(text): return dispatcher.predict(text)
|
requirements.txt
CHANGED
|
@@ -1,19 +1,34 @@
|
|
| 1 |
-
# ---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
|
|
|
|
| 5 |
gradio==4.44.1
|
|
|
|
| 6 |
|
| 7 |
-
# --- LEGACY STACK (
|
|
|
|
| 8 |
tensorflow==2.12.0
|
| 9 |
numpy==1.23.5
|
| 10 |
keras==2.12.0
|
| 11 |
Keras-Preprocessing>=1.1.2
|
| 12 |
|
| 13 |
-
# ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
nltk>=3.8.1
|
|
|
|
|
|
|
|
|
|
| 15 |
opencv-python-headless
|
| 16 |
-
huggingface-hub==0.24.0
|
| 17 |
|
| 18 |
-
# --- MODELS ---
|
|
|
|
|
|
|
| 19 |
https://github.com/explosion/spacy-models/releases/download/it_core_news_lg-3.8.0/it_core_news_lg-3.8.0.tar.gz
|
|
|
|
| 1 |
+
# --- DIRETTIVE DI INSTALLAZIONE (Magia per CPU) ---
|
| 2 |
+
# Questa riga dice a pip di cercare le versioni CPU di Torch, evitando download giganti (CUDA)
|
| 3 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 4 |
|
| 5 |
+
# --- CORE UI ---
|
| 6 |
gradio==4.44.1
|
| 7 |
+
pydantic==2.10.6
|
| 8 |
|
| 9 |
+
# --- LEGACY STACK (NON TOCCARE - Vincoli rigidi) ---
|
| 10 |
+
# TensorFlow 2.12 richiede numpy < 1.24. Teniamo bloccato numpy a 1.23.5
|
| 11 |
tensorflow==2.12.0
|
| 12 |
numpy==1.23.5
|
| 13 |
keras==2.12.0
|
| 14 |
Keras-Preprocessing>=1.1.2
|
| 15 |
|
| 16 |
+
# --- Modulo BPO ---
|
| 17 |
+
# Grazie alla prima riga, scaricherà la versione CPU-only leggera
|
| 18 |
+
torch>=2.0.1
|
| 19 |
+
transformers>=4.35.0
|
| 20 |
+
accelerate>=0.25.0
|
| 21 |
+
|
| 22 |
+
# --- DATA & NLP UTILITIES ---
|
| 23 |
+
pandas>=2.0.0
|
| 24 |
+
spacy==3.8.2
|
| 25 |
nltk>=3.8.1
|
| 26 |
+
scikit-learn>=1.3.0
|
| 27 |
+
|
| 28 |
+
# --- IMAGE PROCESSING ---
|
| 29 |
opencv-python-headless
|
|
|
|
| 30 |
|
| 31 |
+
# --- MODELS & HUB ---
|
| 32 |
+
huggingface-hub==0.24.7
|
| 33 |
+
# Link diretto per scaricare il modello Spacy italiano
|
| 34 |
https://github.com/explosion/spacy-models/releases/download/it_core_news_lg-3.8.0/it_core_news_lg-3.8.0.tar.gz
|