Update app.py
Browse files
app.py
CHANGED
@@ -16,7 +16,7 @@ import plotly.io as pio
|
|
16 |
VB_CSS = r"""
|
17 |
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap');
|
18 |
|
19 |
-
/* Variables Gradio (
|
20 |
:root{
|
21 |
--body-background-fill:#F8FAFC;
|
22 |
--panel-background-fill:#FFFFFF;
|
@@ -29,21 +29,25 @@ VB_CSS = r"""
|
|
29 |
--radius-lg:14px;
|
30 |
--shadow-drop: 0 10px 26px rgba(2,6,23,.08);
|
31 |
|
|
|
32 |
--color-accent:#7C3AED;
|
33 |
--color-accent-soft:#EDE9FE;
|
34 |
--button-primary-background-fill:#7C3AED;
|
35 |
--button-primary-text-color:#ffffff;
|
36 |
|
|
|
37 |
--input-background-fill:#FFFFFF;
|
38 |
--input-border-color:#E2E8F0;
|
39 |
--input-text-color:#0F172A;
|
40 |
--input-placeholder-color:#6B7280;
|
41 |
|
|
|
42 |
--table-row-background-fill:#FFFFFF;
|
43 |
--table-row-text-color:#0F172A;
|
44 |
--table-border-color:#E2E8F0;
|
45 |
}
|
46 |
|
|
|
47 |
:root{
|
48 |
--vb-bg:#F8FAFC;
|
49 |
--vb-card:#FFFFFF;
|
@@ -59,6 +63,7 @@ VB_CSS = r"""
|
|
59 |
--vb-shadow:0 10px 26px rgba(2,6,23,.08);
|
60 |
}
|
61 |
|
|
|
62 |
* { color-scheme: light !important; }
|
63 |
html,body,.gradio-container{
|
64 |
background:var(--vb-bg) !important;
|
@@ -98,7 +103,7 @@ html,body,.gradio-container{
|
|
98 |
box-shadow:var(--vb-shadow);
|
99 |
}
|
100 |
|
101 |
-
/* ---------- Labels / rubans ---------- */
|
102 |
.gradio-container [data-testid="block-label"],
|
103 |
.gradio-container .component-label,
|
104 |
.gradio-container .label,
|
@@ -111,6 +116,8 @@ html,body,.gradio-container{
|
|
111 |
border:none !important;
|
112 |
padding:0 !important;
|
113 |
}
|
|
|
|
|
114 |
.gradio-container .wrap > div[class*="absolute"][class*="top-0"][class*="left-0"],
|
115 |
.gradio-container [class*="absolute"][class*="top-0"][class*="left-0"][class*="rounded-b"],
|
116 |
.gradio-container [class*="absolute"][class*="top-0"][class*="left-0"][class*="rounded-br"]{
|
@@ -120,7 +127,7 @@ html,body,.gradio-container{
|
|
120 |
border:none !important;
|
121 |
}
|
122 |
|
123 |
-
/* ---------- Onglets ---------- */
|
124 |
.gradio-container [role="tablist"],
|
125 |
.gradio-container [role="tab"]{
|
126 |
background:#fff !important;
|
@@ -131,6 +138,7 @@ html,body,.gradio-container{
|
|
131 |
.gradio-container [role="tab"][aria-selected="true"]{
|
132 |
background:linear-gradient(90deg, rgba(124,58,237,.12), rgba(6,182,212,.12)) !important;
|
133 |
color:#0F172A !important;
|
|
|
134 |
}
|
135 |
|
136 |
/* ---------- Textes ---------- */
|
@@ -162,23 +170,9 @@ html,body,.gradio-container{
|
|
162 |
box-shadow:0 0 0 2px rgba(124,58,237,.35), 0 0 0 4px rgba(6,182,212,.25) !important;
|
163 |
}
|
164 |
|
165 |
-
/*
|
166 |
-
|
167 |
-
.gradio-container input[type="
|
168 |
-
accent-color: var(--vb-primary) !important;
|
169 |
-
}
|
170 |
-
/* État coché : on superpose l'icône + le dégradé violet→cyan */
|
171 |
-
.gradio-container input[type="checkbox"]:checked{
|
172 |
-
background-image: var(--checkbox-check), linear-gradient(90deg, var(--vb-primary), var(--vb-primary-2)) !important;
|
173 |
-
background-repeat: no-repeat, no-repeat !important;
|
174 |
-
background-position: center center, center center !important;
|
175 |
-
border-color: var(--vb-primary) !important;
|
176 |
-
}
|
177 |
-
/* Focus & hover cohérents */
|
178 |
-
.gradio-container input[type="checkbox"]:focus-visible{
|
179 |
-
outline: none !important;
|
180 |
-
box-shadow: 0 0 0 2px rgba(124,58,237,.35), 0 0 0 4px rgba(6,182,212,.25) !important;
|
181 |
-
}
|
182 |
|
183 |
/* ---------- Sliders ---------- */
|
184 |
.gradio-container .noUi-target{
|
@@ -205,30 +199,22 @@ html,body,.gradio-container{
|
|
205 |
}
|
206 |
.gradio-container .vb-cta:hover{transform:translateY(-2px);filter:brightness(1.05)}
|
207 |
|
208 |
-
/* ---------- DataFrames / Tables
|
209 |
-
.gradio-container
|
210 |
-
|
211 |
-
.gradio-container .svelte-virtual-table-viewport,
|
212 |
-
.gradio-container .table-wrap{
|
213 |
-
background:#fff !important;
|
214 |
-
color:#0F172A !important;
|
215 |
-
border-color:#E2E8F0 !important;
|
216 |
}
|
217 |
-
.gradio-container
|
218 |
-
|
219 |
-
|
|
|
220 |
background:linear-gradient(90deg, rgba(124,58,237,.12), rgba(6,182,212,.12)) !important;
|
221 |
-
color:#
|
222 |
}
|
223 |
-
.gradio-container .
|
224 |
-
|
225 |
-
.gradio-container .header-button{
|
226 |
-
background:transparent !important; color:#0F172A !important;
|
227 |
-
box-shadow:none !important; border:none !important;
|
228 |
}
|
229 |
-
.gradio-container .table caption{color:#0F172A !important}
|
230 |
|
231 |
-
/* ---------- Files ---------- */
|
232 |
.gradio-container [class*="file"]{
|
233 |
background:#fff !important; color:#0F172A !important;
|
234 |
border-color:var(--vb-border) !important;
|
@@ -271,6 +257,7 @@ LOGO_SVG = """<svg xmlns='http://www.w3.org/2000/svg' width='224' height='38' vi
|
|
271 |
</svg>"""
|
272 |
|
273 |
# ====================== UNIDECODE (fallback) ======================
|
|
|
274 |
try:
|
275 |
from unidecode import unidecode
|
276 |
except Exception:
|
@@ -282,6 +269,7 @@ except Exception:
|
|
282 |
return str(x)
|
283 |
|
284 |
# ====================== THÉSAURUS ASSURANCE ======================
|
|
|
285 |
THEMES = {
|
286 |
"Remboursements santé":[r"\bremboursement[s]?\b", r"\bt[eé]l[eé]transmission\b", r"\bno[eé]mie\b",
|
287 |
r"\bprise\s*en\s*charge[s]?\b", r"\btaux\s+de\s+remboursement[s]?\b", r"\b(ameli|cpam)\b",
|
@@ -313,6 +301,7 @@ THEMES = {
|
|
313 |
}
|
314 |
|
315 |
# ====================== SENTIMENT (règles) ======================
|
|
|
316 |
POS_WORDS = {"bien":1.0,"super":1.2,"parfait":1.4,"excellent":1.5,"ravi":1.2,"satisfait":1.0,
|
317 |
"rapide":0.8,"efficace":1.0,"fiable":1.0,"simple":0.8,"facile":0.8,"clair":0.8,"conforme":0.8,
|
318 |
"sympa":0.8,"professionnel":1.0,"réactif":1.0,"reactif":1.0,"compétent":1.0,"competent":1.0,
|
@@ -327,6 +316,7 @@ DIMINISHERS = [r"\bun[e]?\s+peu\b", r"\bassez\b", r"\bplut[oô]t\b", r"\bl[eé]
|
|
327 |
INTENSIFIER_W, DIMINISHER_W = 1.5, 0.7
|
328 |
|
329 |
# ====================== OpenAI (optionnel) ======================
|
|
|
330 |
OPENAI_AVAILABLE = False
|
331 |
try:
|
332 |
from openai import OpenAI
|
@@ -336,6 +326,7 @@ except Exception:
|
|
336 |
OPENAI_AVAILABLE = False
|
337 |
|
338 |
# ====================== UTILS ======================
|
|
|
339 |
def normalize(t:str)->str:
|
340 |
if not isinstance(t,str): return ""
|
341 |
return re.sub(r"\s+"," ",t.strip())
|
@@ -460,6 +451,7 @@ def infer_nps_from_sentiment(label: str, score: float) -> int:
|
|
460 |
return 8 if score >= 0 else 7
|
461 |
|
462 |
# ====================== GRAPHIQUES ======================
|
|
|
463 |
def fig_nps_gauge(nps: Optional[float]) -> go.Figure:
|
464 |
v = 0.0 if nps is None else float(nps)
|
465 |
return go.Figure(go.Indicator(mode="gauge+number", value=v,
|
@@ -485,6 +477,7 @@ def fig_theme_balance(themes_df: pd.DataFrame, k: int) -> go.Figure:
|
|
485 |
fig.update_layout(xaxis_tickangle=-30); return fig
|
486 |
|
487 |
# ====================== ANALYSE ======================
|
|
|
488 |
def analyze_text(pasted_txt, has_sc, sep_chr,
|
489 |
do_anonymize, use_oa_sent, use_oa_themes, use_oa_summary,
|
490 |
oa_model, oa_temp, top_k):
|
@@ -584,7 +577,7 @@ def analyze_text(pasted_txt, has_sc, sep_chr,
|
|
584 |
nps_label = "NPS global (inféré)" if any_inferred else "NPS global"
|
585 |
lines=[ "# Synthèse NPS & ressentis clients",
|
586 |
f"- **Méthode** : {method}",
|
587 |
-
f"- **{nps_label}** : {nps:.1f}" if nps is not None else f"- **{
|
588 |
if dist:
|
589 |
tot=sum(dist.values()); pos=dist.get("positive",0); neg=dist.get("negatif",0); neu=dist.get("neutre",0)
|
590 |
lines.append(f"- **Répartition émotions** : positive {pos}/{tot}, neutre {neu}/{tot}, négative {neg}/{tot}")
|
@@ -648,10 +641,7 @@ def analyze_text(pasted_txt, has_sc, sep_chr,
|
|
648 |
|
649 |
# ====================== UI ======================
|
650 |
|
651 |
-
|
652 |
-
apply_plotly_theme()
|
653 |
-
|
654 |
-
apply_plotly_theme_wrapper()
|
655 |
|
656 |
with gr.Blocks(title="Verbatify — Analyse NPS", css=VB_CSS) as demo:
|
657 |
gr.HTML(
|
|
|
16 |
VB_CSS = r"""
|
17 |
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap');
|
18 |
|
19 |
+
/* ---------- Variables Gradio (écrasent les thèmes host) ---------- */
|
20 |
:root{
|
21 |
--body-background-fill:#F8FAFC;
|
22 |
--panel-background-fill:#FFFFFF;
|
|
|
29 |
--radius-lg:14px;
|
30 |
--shadow-drop: 0 10px 26px rgba(2,6,23,.08);
|
31 |
|
32 |
+
/* Accent (pas d’orange) */
|
33 |
--color-accent:#7C3AED;
|
34 |
--color-accent-soft:#EDE9FE;
|
35 |
--button-primary-background-fill:#7C3AED;
|
36 |
--button-primary-text-color:#ffffff;
|
37 |
|
38 |
+
/* Inputs */
|
39 |
--input-background-fill:#FFFFFF;
|
40 |
--input-border-color:#E2E8F0;
|
41 |
--input-text-color:#0F172A;
|
42 |
--input-placeholder-color:#6B7280;
|
43 |
|
44 |
+
/* Tables */
|
45 |
--table-row-background-fill:#FFFFFF;
|
46 |
--table-row-text-color:#0F172A;
|
47 |
--table-border-color:#E2E8F0;
|
48 |
}
|
49 |
|
50 |
+
/* Palette dégradé violet → cyan */
|
51 |
:root{
|
52 |
--vb-bg:#F8FAFC;
|
53 |
--vb-card:#FFFFFF;
|
|
|
63 |
--vb-shadow:0 10px 26px rgba(2,6,23,.08);
|
64 |
}
|
65 |
|
66 |
+
/* Forcer un look clair */
|
67 |
* { color-scheme: light !important; }
|
68 |
html,body,.gradio-container{
|
69 |
background:var(--vb-bg) !important;
|
|
|
103 |
box-shadow:var(--vb-shadow);
|
104 |
}
|
105 |
|
106 |
+
/* ---------- Labels / rubans (supprime les pills noires) ---------- */
|
107 |
.gradio-container [data-testid="block-label"],
|
108 |
.gradio-container .component-label,
|
109 |
.gradio-container .label,
|
|
|
116 |
border:none !important;
|
117 |
padding:0 !important;
|
118 |
}
|
119 |
+
|
120 |
+
/* Ribbons collés en haut à gauche (bg-* neutral/gray…) */
|
121 |
.gradio-container .wrap > div[class*="absolute"][class*="top-0"][class*="left-0"],
|
122 |
.gradio-container [class*="absolute"][class*="top-0"][class*="left-0"][class*="rounded-b"],
|
123 |
.gradio-container [class*="absolute"][class*="top-0"][class*="left-0"][class*="rounded-br"]{
|
|
|
127 |
border:none !important;
|
128 |
}
|
129 |
|
130 |
+
/* ---------- Onglets / pagination internes (DataFrame, etc.) ---------- */
|
131 |
.gradio-container [role="tablist"],
|
132 |
.gradio-container [role="tab"]{
|
133 |
background:#fff !important;
|
|
|
138 |
.gradio-container [role="tab"][aria-selected="true"]{
|
139 |
background:linear-gradient(90deg, rgba(124,58,237,.12), rgba(6,182,212,.12)) !important;
|
140 |
color:#0F172A !important;
|
141 |
+
border-color:var(--vb-border) !important;
|
142 |
}
|
143 |
|
144 |
/* ---------- Textes ---------- */
|
|
|
170 |
box-shadow:0 0 0 2px rgba(124,58,237,.35), 0 0 0 4px rgba(6,182,212,.25) !important;
|
171 |
}
|
172 |
|
173 |
+
/* Checkboxes / radios */
|
174 |
+
.gradio-container input[type="checkbox"],
|
175 |
+
.gradio-container input[type="radio"]{ accent-color:var(--vb-primary) !important }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
176 |
|
177 |
/* ---------- Sliders ---------- */
|
178 |
.gradio-container .noUi-target{
|
|
|
199 |
}
|
200 |
.gradio-container .vb-cta:hover{transform:translateY(-2px);filter:brightness(1.05)}
|
201 |
|
202 |
+
/* ---------- DataFrames / Tables ---------- */
|
203 |
+
.gradio-container [data-testid="dataframe"]{
|
204 |
+
background:#fff !important; border:1px solid var(--vb-border) !important; border-radius:var(--vb-radius) !important;
|
|
|
|
|
|
|
|
|
|
|
205 |
}
|
206 |
+
.gradio-container [data-testid="dataframe"] *{
|
207 |
+
background:#fff !important; color:#0F172A !important; border-color:#E2E8F0 !important;
|
208 |
+
}
|
209 |
+
.gradio-container thead th{
|
210 |
background:linear-gradient(90deg, rgba(124,58,237,.12), rgba(6,182,212,.12)) !important;
|
211 |
+
color:#111827 !important; font-weight:800 !important; border-bottom:1px solid var(--vb-border) !important;
|
212 |
}
|
213 |
+
.gradio-container td, .gradio-container th{
|
214 |
+
padding:10px !important; border-bottom:1px solid #F1F5F9 !important;
|
|
|
|
|
|
|
215 |
}
|
|
|
216 |
|
217 |
+
/* ---------- Fichiers (gr.Files) ---------- */
|
218 |
.gradio-container [class*="file"]{
|
219 |
background:#fff !important; color:#0F172A !important;
|
220 |
border-color:var(--vb-border) !important;
|
|
|
257 |
</svg>"""
|
258 |
|
259 |
# ====================== UNIDECODE (fallback) ======================
|
260 |
+
|
261 |
try:
|
262 |
from unidecode import unidecode
|
263 |
except Exception:
|
|
|
269 |
return str(x)
|
270 |
|
271 |
# ====================== THÉSAURUS ASSURANCE ======================
|
272 |
+
|
273 |
THEMES = {
|
274 |
"Remboursements santé":[r"\bremboursement[s]?\b", r"\bt[eé]l[eé]transmission\b", r"\bno[eé]mie\b",
|
275 |
r"\bprise\s*en\s*charge[s]?\b", r"\btaux\s+de\s+remboursement[s]?\b", r"\b(ameli|cpam)\b",
|
|
|
301 |
}
|
302 |
|
303 |
# ====================== SENTIMENT (règles) ======================
|
304 |
+
|
305 |
POS_WORDS = {"bien":1.0,"super":1.2,"parfait":1.4,"excellent":1.5,"ravi":1.2,"satisfait":1.0,
|
306 |
"rapide":0.8,"efficace":1.0,"fiable":1.0,"simple":0.8,"facile":0.8,"clair":0.8,"conforme":0.8,
|
307 |
"sympa":0.8,"professionnel":1.0,"réactif":1.0,"reactif":1.0,"compétent":1.0,"competent":1.0,
|
|
|
316 |
INTENSIFIER_W, DIMINISHER_W = 1.5, 0.7
|
317 |
|
318 |
# ====================== OpenAI (optionnel) ======================
|
319 |
+
|
320 |
OPENAI_AVAILABLE = False
|
321 |
try:
|
322 |
from openai import OpenAI
|
|
|
326 |
OPENAI_AVAILABLE = False
|
327 |
|
328 |
# ====================== UTILS ======================
|
329 |
+
|
330 |
def normalize(t:str)->str:
|
331 |
if not isinstance(t,str): return ""
|
332 |
return re.sub(r"\s+"," ",t.strip())
|
|
|
451 |
return 8 if score >= 0 else 7
|
452 |
|
453 |
# ====================== GRAPHIQUES ======================
|
454 |
+
|
455 |
def fig_nps_gauge(nps: Optional[float]) -> go.Figure:
|
456 |
v = 0.0 if nps is None else float(nps)
|
457 |
return go.Figure(go.Indicator(mode="gauge+number", value=v,
|
|
|
477 |
fig.update_layout(xaxis_tickangle=-30); return fig
|
478 |
|
479 |
# ====================== ANALYSE ======================
|
480 |
+
|
481 |
def analyze_text(pasted_txt, has_sc, sep_chr,
|
482 |
do_anonymize, use_oa_sent, use_oa_themes, use_oa_summary,
|
483 |
oa_model, oa_temp, top_k):
|
|
|
577 |
nps_label = "NPS global (inféré)" if any_inferred else "NPS global"
|
578 |
lines=[ "# Synthèse NPS & ressentis clients",
|
579 |
f"- **Méthode** : {method}",
|
580 |
+
f"- **{nps_label}** : {nps:.1f}" if nps is not None else f"- **{npslabel}** : n/a" ]
|
581 |
if dist:
|
582 |
tot=sum(dist.values()); pos=dist.get("positive",0); neg=dist.get("negatif",0); neu=dist.get("neutre",0)
|
583 |
lines.append(f"- **Répartition émotions** : positive {pos}/{tot}, neutre {neu}/{tot}, négative {neg}/{tot}")
|
|
|
641 |
|
642 |
# ====================== UI ======================
|
643 |
|
644 |
+
apply_plotly_theme()
|
|
|
|
|
|
|
645 |
|
646 |
with gr.Blocks(title="Verbatify — Analyse NPS", css=VB_CSS) as demo:
|
647 |
gr.HTML(
|