Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
CHANGED
|
@@ -11,7 +11,6 @@ from huggingface_hub import InferenceClient
|
|
| 11 |
MODEL_ID = os.environ.get("MODEL_ID", "meta-llama/Meta-Llama-3.1-8B-Instruct")
|
| 12 |
HF_TOKEN = os.environ.get("HF_TOKEN") # set in Space settings → Repository secrets
|
| 13 |
|
| 14 |
-
# ────────────────────────────────────────────────────────────────────────────────
|
| 15 |
CATEGORIES = [
|
| 16 |
{"key": "alimentation", "icon": "🍎", "fr": "Alimentation", "en": "Nutrition"},
|
| 17 |
{"key": "mouvement", "icon": "🦘", "fr": "Mouvement", "en": "Movement"},
|
|
@@ -20,25 +19,24 @@ CATEGORIES = [
|
|
| 20 |
{"key": "bien-etre", "icon": "💬", "fr": "Bien-être", "en": "Well-being"},
|
| 21 |
]
|
| 22 |
|
| 23 |
-
# Category-specific guidance (stronger prompt conditioning)
|
| 24 |
GUIDES = {
|
| 25 |
"fr": {
|
| 26 |
-
"alimentation": "
|
| 27 |
-
"mouvement": "
|
| 28 |
-
"cerveau": "Stimulation douce: curiosité,
|
| 29 |
-
"liens": "Interactions
|
| 30 |
-
"bien-etre": "
|
| 31 |
},
|
| 32 |
"en": {
|
| 33 |
-
"alimentation": "
|
| 34 |
-
"mouvement": "
|
| 35 |
-
"cerveau": "Gentle stimulation: curiosity,
|
| 36 |
-
"liens": "Simple
|
| 37 |
-
"bien-etre": "Micro
|
| 38 |
},
|
| 39 |
}
|
| 40 |
|
| 41 |
-
# Few-shot examples
|
| 42 |
FEWSHOTS = {
|
| 43 |
"fr": {
|
| 44 |
"alimentation": {
|
|
@@ -46,6 +44,7 @@ FEWSHOTS = {
|
|
| 46 |
"Quelle boisson te donne envie de boire plus d’eau dans la journée ?",
|
| 47 |
"Quel ajout simple rend ton petit-déj plus rassasiant ?",
|
| 48 |
"Quand as-tu naturellement faim d’un fruit ou d’un yaourt ?",
|
|
|
|
| 49 |
],
|
| 50 |
"micro_actions": [
|
| 51 |
"Remplir une gourde ce matin.",
|
|
@@ -57,6 +56,7 @@ FEWSHOTS = {
|
|
| 57 |
"Quel trajet pourrais-tu faire à pied au moins une fois cette semaine ?",
|
| 58 |
"Quelle pause-active de 2 minutes peux-tu glisser entre deux tâches ?",
|
| 59 |
"Qu’est-ce qui te fait bouger sans y penser (ex: marcher au téléphone) ?",
|
|
|
|
| 60 |
],
|
| 61 |
"micro_actions": [
|
| 62 |
"Monter un étage par les escaliers aujourd’hui.",
|
|
@@ -68,6 +68,7 @@ FEWSHOTS = {
|
|
| 68 |
"Qu’est-ce qui a suscité ta curiosité aujourd’hui ?",
|
| 69 |
"Quel moment t’irait pour 3 minutes de respiration ?",
|
| 70 |
"Quel mini-jeu aimes-tu pour réveiller l’esprit (ex: 3 mots fléchés) ?",
|
|
|
|
| 71 |
],
|
| 72 |
"micro_actions": [
|
| 73 |
"Programmer un minuteur de 3 minutes pour respirer.",
|
|
@@ -79,6 +80,7 @@ FEWSHOTS = {
|
|
| 79 |
"Qui pourrais-tu remercier aujourd’hui et comment ?",
|
| 80 |
"À qui enverrais-tu un message court pour reprendre contact ?",
|
| 81 |
"Avec qui partagerais-tu une courte marche cette semaine ?",
|
|
|
|
| 82 |
],
|
| 83 |
"micro_actions": [
|
| 84 |
"Envoyer un message de gratitude à une personne.",
|
|
@@ -90,6 +92,7 @@ FEWSHOTS = {
|
|
| 90 |
"Quel signal t’indique qu’il est temps de faire une pause ?",
|
| 91 |
"Quelle routine de 2 minutes t’aide à te recentrer ?",
|
| 92 |
"Quel moment favorise un coucher plus régulier ?",
|
|
|
|
| 93 |
],
|
| 94 |
"micro_actions": [
|
| 95 |
"Éteindre les écrans 10 minutes plus tôt ce soir.",
|
|
@@ -103,6 +106,7 @@ FEWSHOTS = {
|
|
| 103 |
"What drink makes you want to sip more water through the day?",
|
| 104 |
"What small add-on makes your breakfast more filling?",
|
| 105 |
"When do you naturally crave a fruit or yogurt?",
|
|
|
|
| 106 |
],
|
| 107 |
"micro_actions": [
|
| 108 |
"Fill a water bottle this morning.",
|
|
@@ -114,6 +118,7 @@ FEWSHOTS = {
|
|
| 114 |
"Which short trip could you walk at least once this week?",
|
| 115 |
"Which 2-minute active break fits between two tasks?",
|
| 116 |
"What makes you move without noticing (e.g., walking on calls)?",
|
|
|
|
| 117 |
],
|
| 118 |
"micro_actions": [
|
| 119 |
"Take one flight of stairs today.",
|
|
@@ -125,6 +130,7 @@ FEWSHOTS = {
|
|
| 125 |
"What sparked your curiosity today?",
|
| 126 |
"When could you do 3 minutes of breathing?",
|
| 127 |
"Which mini-game wakes you up (e.g., 3 crossword clues)?",
|
|
|
|
| 128 |
],
|
| 129 |
"micro_actions": [
|
| 130 |
"Set a 3-minute timer to breathe.",
|
|
@@ -136,6 +142,7 @@ FEWSHOTS = {
|
|
| 136 |
"Who could you thank today—and how?",
|
| 137 |
"Who might you text briefly to reconnect?",
|
| 138 |
"Who could you invite for a short walk this week?",
|
|
|
|
| 139 |
],
|
| 140 |
"micro_actions": [
|
| 141 |
"Send a gratitude message to one person.",
|
|
@@ -147,6 +154,7 @@ FEWSHOTS = {
|
|
| 147 |
"What cue tells you it’s time for a pause?",
|
| 148 |
"What 2-minute routine helps you reset?",
|
| 149 |
"What time supports a steadier bedtime?",
|
|
|
|
| 150 |
],
|
| 151 |
"micro_actions": [
|
| 152 |
"Turn screens off 10 minutes earlier tonight.",
|
|
@@ -157,7 +165,7 @@ FEWSHOTS = {
|
|
| 157 |
}
|
| 158 |
|
| 159 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 160 |
-
# PROMPT & MODEL
|
| 161 |
|
| 162 |
|
| 163 |
def build_prompt(lang: str, category_key: str, variant: str) -> str:
|
|
@@ -170,21 +178,22 @@ def build_prompt(lang: str, category_key: str, variant: str) -> str:
|
|
| 170 |
few = FEWSHOTS[lang][category_key]
|
| 171 |
|
| 172 |
variant_fr = (
|
| 173 |
-
"
|
| 174 |
if variant == "best"
|
| 175 |
-
else "
|
| 176 |
)
|
| 177 |
variant_en = (
|
| 178 |
-
"Tone
|
| 179 |
if variant == "best"
|
| 180 |
-
else "Tone
|
| 181 |
)
|
| 182 |
|
|
|
|
| 183 |
schema = (
|
| 184 |
"{\n"
|
| 185 |
' "category": "<category_key>",\n'
|
| 186 |
' "language": "<fr|en>",\n'
|
| 187 |
-
' "questions": ["q1", "q2", "q3"],\n'
|
| 188 |
' "micro_actions": ["m1", "m2"],\n'
|
| 189 |
' "tone": "playful|sincere|ludique|sincère",\n'
|
| 190 |
' "safety_notes": "short coaching tips"\n'
|
|
@@ -194,62 +203,43 @@ def build_prompt(lang: str, category_key: str, variant: str) -> str:
|
|
| 194 |
if lang == "fr":
|
| 195 |
system = (
|
| 196 |
"Tu es l’IA du jeu de cartes Neurovie, inspiré du modèle FINGER. "
|
| 197 |
-
"Une carte = une question sur les routines
|
| 198 |
-
)
|
| 199 |
-
rules = (
|
| 200 |
-
"Mode d’emploi (style 7 familles):\n"
|
| 201 |
-
"- Demande une carte (catégorie FINGER) avec une question précise.\n"
|
| 202 |
-
"- Si l’autre joueur l’a, il la donne et continue. Sinon, tu pioches.\n"
|
| 203 |
-
"Variante: chacun pioche à son tour; le meilleur ou le plus sincère garde la carte."
|
| 204 |
)
|
| 205 |
safety = (
|
| 206 |
-
"Règles
|
| 207 |
-
"-
|
| 208 |
-
"- Langage bienveillant, inclusif,
|
| 209 |
-
"-
|
| 210 |
-
"- Phrases courtes. Pas d’emojis. Pas de texte hors JSON.\n"
|
| 211 |
)
|
| 212 |
user = (
|
| 213 |
f"Catégorie: {cat['fr']} {cat['icon']}. {variant_fr}\n"
|
| 214 |
-
f"
|
| 215 |
-
"
|
| 216 |
-
"Respecte le schéma JSON ci-dessous. Imites le style, pas le contenu exact, des exemples.\n"
|
| 217 |
f"Exemples de style (ne pas copier mot à mot): questions={few['questions']} micro_actions={few['micro_actions']}\n"
|
| 218 |
-
"Schéma JSON strict:\n"
|
| 219 |
-
f"{schema}\n"
|
| 220 |
"RENVOIE UNIQUEMENT DU JSON VALIDE."
|
| 221 |
)
|
| 222 |
-
return f"{system}\n\n{rules}\n\n{safety}\n\n{user}"
|
| 223 |
-
|
| 224 |
else:
|
| 225 |
system = (
|
| 226 |
"You are the AI for the Neurovie card game, inspired by the FINGER model. "
|
| 227 |
-
"One card = one question about daily routines
|
| 228 |
-
)
|
| 229 |
-
rules = (
|
| 230 |
-
"How to play (7-families style):\n"
|
| 231 |
-
"- Ask for a specific card (FINGER category) using a clear question.\n"
|
| 232 |
-
"- If they have it, they give it and keep asking; otherwise, draw.\n"
|
| 233 |
-
"Variant: players draw in turn; the best or most sincere keeps the card."
|
| 234 |
)
|
| 235 |
safety = (
|
| 236 |
-
"
|
| 237 |
-
"- No medical advice
|
| 238 |
-
"- Kind,
|
| 239 |
-
"-
|
| 240 |
-
"- Short sentences. No emojis. No text outside JSON.\n"
|
| 241 |
)
|
| 242 |
user = (
|
| 243 |
f"Category: {cat['en']} {cat['icon']}. {variant_en}\n"
|
| 244 |
-
f"
|
| 245 |
-
"
|
| 246 |
-
"Follow the JSON schema below. Imitate style, not exact wording, of examples.\n"
|
| 247 |
f"Style examples (do not copy verbatim): questions={few['questions']} micro_actions={few['micro_actions']}\n"
|
| 248 |
-
"Strict JSON schema:\n"
|
| 249 |
-
f"{schema}\n"
|
| 250 |
"RETURN VALID JSON ONLY."
|
| 251 |
)
|
| 252 |
-
|
|
|
|
| 253 |
|
| 254 |
|
| 255 |
def try_parse_json(text: str) -> Optional[Dict[str, Any]]:
|
|
@@ -270,7 +260,8 @@ def normalize_output(
|
|
| 270 |
q = [str(x).strip() for x in data.get("questions", []) if str(x).strip()]
|
| 271 |
m = [str(x).strip() for x in data.get("micro_actions", []) if str(x).strip()]
|
| 272 |
|
| 273 |
-
|
|
|
|
| 274 |
m = (m + [""] * 2)[:2]
|
| 275 |
|
| 276 |
if not data.get("tone"):
|
|
@@ -302,7 +293,6 @@ def normalize_output(
|
|
| 302 |
def model_call(prompt: str) -> str:
|
| 303 |
client = InferenceClient(model=MODEL_ID, token=HF_TOKEN)
|
| 304 |
|
| 305 |
-
# Try chat-style first
|
| 306 |
try:
|
| 307 |
resp = client.chat.completions.create(
|
| 308 |
model=MODEL_ID,
|
|
@@ -326,7 +316,6 @@ def model_call(prompt: str) -> str:
|
|
| 326 |
except Exception:
|
| 327 |
pass
|
| 328 |
|
| 329 |
-
# Fallback to text_generation
|
| 330 |
return client.text_generation(
|
| 331 |
prompt,
|
| 332 |
max_new_tokens=220,
|
|
@@ -336,7 +325,7 @@ def model_call(prompt: str) -> str:
|
|
| 336 |
).strip()
|
| 337 |
|
| 338 |
|
| 339 |
-
def generate(lang: str, category_key: str, variant: str) -> Dict[str,
|
| 340 |
prompt = build_prompt(lang, category_key, variant)
|
| 341 |
|
| 342 |
raw_text = None
|
|
@@ -352,7 +341,7 @@ def generate(lang: str, category_key: str, variant: str) -> Dict[str, str]:
|
|
| 352 |
parsed = {
|
| 353 |
"category": category_key,
|
| 354 |
"language": lang,
|
| 355 |
-
"questions": few["questions"][:
|
| 356 |
"micro_actions": few["micro_actions"][:2],
|
| 357 |
"tone": (
|
| 358 |
"ludique"
|
|
@@ -372,55 +361,44 @@ def generate(lang: str, category_key: str, variant: str) -> Dict[str, str]:
|
|
| 372 |
|
| 373 |
normalized = normalize_output(parsed, lang, category_key, variant)
|
| 374 |
|
| 375 |
-
lines: List[str] = [
|
| 376 |
-
f"Category: {normalized['category']} | Lang: {normalized['language']} | Tone: {normalized['tone']}",
|
| 377 |
-
"",
|
| 378 |
-
"Questions:",
|
| 379 |
-
]
|
| 380 |
-
lines.extend(f"• {q}" for q in normalized["questions"])
|
| 381 |
-
lines.append("")
|
| 382 |
-
lines.append("Micro-actions:")
|
| 383 |
-
lines.extend(f"• {m}" for m in normalized["micro_actions"])
|
| 384 |
-
if normalized.get("safety_notes"):
|
| 385 |
-
lines.append("")
|
| 386 |
-
lines.append(f"Notes: {normalized['safety_notes']}")
|
| 387 |
-
|
| 388 |
return {
|
| 389 |
-
"
|
|
|
|
| 390 |
"raw_json": json.dumps(normalized, ensure_ascii=False, indent=2),
|
| 391 |
}
|
| 392 |
|
| 393 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 394 |
-
# UI –
|
|
|
|
| 395 |
|
| 396 |
CUSTOM_CSS = """
|
| 397 |
:root {
|
| 398 |
-
--nv-bg: #
|
| 399 |
--nv-card: #fffdf8;
|
| 400 |
-
--nv-border: #
|
| 401 |
-
--nv-accent: #
|
| 402 |
-
--nv-accent-soft: #
|
| 403 |
-
--nv-accent-teal: #
|
|
|
|
| 404 |
--nv-text-main: #262626;
|
| 405 |
--nv-text-muted: #6c6459;
|
| 406 |
}
|
| 407 |
|
| 408 |
-
/* background + blobs */
|
| 409 |
.gradio-container {
|
| 410 |
font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
|
| 411 |
-
background: radial-gradient(circle at top left, #
|
| 412 |
-
max-width:
|
| 413 |
margin: 0 auto !important;
|
| 414 |
padding: 32px 0 40px 0;
|
| 415 |
position: relative;
|
| 416 |
}
|
| 417 |
|
|
|
|
| 418 |
@keyframes nvBlobFloat {
|
| 419 |
-
0% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.
|
| 420 |
50% { transform: translate3d(10px, -8px, 0) scale(1.04); opacity: 0.9; }
|
| 421 |
-
100% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.
|
| 422 |
}
|
| 423 |
-
|
| 424 |
.gradio-container::before,
|
| 425 |
.gradio-container::after {
|
| 426 |
content: "";
|
|
@@ -435,16 +413,16 @@ CUSTOM_CSS = """
|
|
| 435 |
.gradio-container::before {
|
| 436 |
top: -80px;
|
| 437 |
left: -60px;
|
| 438 |
-
background: radial-gradient(circle at 30% 30%, #
|
| 439 |
}
|
| 440 |
.gradio-container::after {
|
| 441 |
bottom: -120px;
|
| 442 |
right: -40px;
|
| 443 |
-
background: radial-gradient(circle at 70% 70%, #
|
| 444 |
animation-delay: 4s;
|
| 445 |
}
|
| 446 |
|
| 447 |
-
/* main
|
| 448 |
@keyframes nvCardIn {
|
| 449 |
0% { opacity: 0; transform: translateY(8px) scale(0.98); }
|
| 450 |
100% { opacity: 1; transform: translateY(0) scale(1); }
|
|
@@ -504,18 +482,18 @@ CUSTOM_CSS = """
|
|
| 504 |
transition: background 150ms ease, transform 120ms ease, box-shadow 120ms ease;
|
| 505 |
}
|
| 506 |
.nv-pills input:checked + label {
|
| 507 |
-
background: linear-gradient(135deg, #
|
| 508 |
border-color: var(--nv-accent) !important;
|
| 509 |
color: var(--nv-text-main) !important;
|
| 510 |
-
box-shadow: 0 4px 10px rgba(
|
| 511 |
transform: translateY(-1px);
|
| 512 |
}
|
| 513 |
|
| 514 |
/* button */
|
| 515 |
@keyframes nvPulse {
|
| 516 |
-
0% { transform: translateY(0) scale(1); box-shadow: 0 10px 22px rgba(
|
| 517 |
-
50% { transform: translateY(-1px) scale(1.02); box-shadow: 0 14px 26px rgba(
|
| 518 |
-
100% { transform: translateY(0) scale(1); box-shadow: 0 10px 22px rgba(
|
| 519 |
}
|
| 520 |
.gr-button {
|
| 521 |
border-radius: 999px !important;
|
|
@@ -524,8 +502,8 @@ CUSTOM_CSS = """
|
|
| 524 |
font-size: 0.96rem !important;
|
| 525 |
border: none !important;
|
| 526 |
background:
|
| 527 |
-
radial-gradient(circle at 0 0, #
|
| 528 |
-
linear-gradient(135deg, #f6a37d, #
|
| 529 |
color: #fff !important;
|
| 530 |
animation: nvPulse 4s ease-in-out infinite;
|
| 531 |
}
|
|
@@ -533,21 +511,43 @@ CUSTOM_CSS = """
|
|
| 533 |
animation-duration: 1.4s;
|
| 534 |
}
|
| 535 |
|
| 536 |
-
/*
|
| 537 |
-
@keyframes
|
| 538 |
-
0% {
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
}
|
| 541 |
-
.nv-
|
| 542 |
-
background: #
|
| 543 |
border-radius: 18px;
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
}
|
| 549 |
|
| 550 |
-
/*
|
| 551 |
.nv-json code, .nv-json textarea {
|
| 552 |
border-radius: 16px !important;
|
| 553 |
}
|
|
@@ -578,11 +578,15 @@ def _map_category(choice: str) -> str:
|
|
| 578 |
def click_handler(l, c, v):
|
| 579 |
c_key = _map_category(c)
|
| 580 |
out = generate(l, c_key, v)
|
| 581 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 582 |
|
| 583 |
|
| 584 |
with gr.Blocks(title="Neurovie – Question Studio") as demo:
|
| 585 |
-
# inject CSS
|
| 586 |
gr.HTML(f"<style>{CUSTOM_CSS}</style>")
|
| 587 |
|
| 588 |
with gr.Column(elem_classes="nv-shell"):
|
|
@@ -592,12 +596,13 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
|
|
| 592 |
<div class="nv-badge">NEUROVIE · FINGER</div>
|
| 593 |
<div class="nv-title">Question Studio</div>
|
| 594 |
<div class="nv-subtitle">
|
| 595 |
-
Minimal prompts for rich conversations — draw
|
| 596 |
</div>
|
| 597 |
</div>
|
| 598 |
"""
|
| 599 |
)
|
| 600 |
|
|
|
|
| 601 |
with gr.Row(elem_classes="nv-section"):
|
| 602 |
with gr.Column():
|
| 603 |
gr.HTML("<div class='nv-label'>Language</div>")
|
|
@@ -633,15 +638,28 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
|
|
| 633 |
|
| 634 |
btn = gr.Button("Generate card set ✨")
|
| 635 |
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 644 |
|
|
|
|
| 645 |
with gr.Column(elem_classes=["nv-section", "nv-json"]):
|
| 646 |
gr.HTML("<div class='nv-label'>JSON (for dev)</div>")
|
| 647 |
raw_json = gr.Code(
|
|
@@ -650,7 +668,32 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
|
|
| 650 |
show_label=False,
|
| 651 |
)
|
| 652 |
|
| 653 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
|
| 655 |
if __name__ == "__main__":
|
| 656 |
demo.launch()
|
|
|
|
| 11 |
MODEL_ID = os.environ.get("MODEL_ID", "meta-llama/Meta-Llama-3.1-8B-Instruct")
|
| 12 |
HF_TOKEN = os.environ.get("HF_TOKEN") # set in Space settings → Repository secrets
|
| 13 |
|
|
|
|
| 14 |
CATEGORIES = [
|
| 15 |
{"key": "alimentation", "icon": "🍎", "fr": "Alimentation", "en": "Nutrition"},
|
| 16 |
{"key": "mouvement", "icon": "🦘", "fr": "Mouvement", "en": "Movement"},
|
|
|
|
| 19 |
{"key": "bien-etre", "icon": "💬", "fr": "Bien-être", "en": "Well-being"},
|
| 20 |
]
|
| 21 |
|
|
|
|
| 22 |
GUIDES = {
|
| 23 |
"fr": {
|
| 24 |
+
"alimentation": "Habitudes simples: hydratation, fruits/légumes, collations, rythme des repas. Pas de régime strict, pas de moralisation.",
|
| 25 |
+
"mouvement": "Mouvement du quotidien: marche, escaliers, étirements courts, pauses actives. Pas de performance sportive.",
|
| 26 |
+
"cerveau": "Stimulation douce: curiosité, respiration, mini-jeux, petit apprentissage. Zéro jargon médical.",
|
| 27 |
+
"liens": "Interactions simples: gratitude, messages courts, appels brefs, moments partagés. Ton chaleureux, inclusif.",
|
| 28 |
+
"bien-etre": "Micro bien-être: pauses, sommeil régulier, respirations, petits rituels qui apaisent. Jamais culpabilisant.",
|
| 29 |
},
|
| 30 |
"en": {
|
| 31 |
+
"alimentation": "Simple habits: hydration, fruit/veg, snacks, meal rhythm. No strict diets, no moralizing.",
|
| 32 |
+
"mouvement": "Daily movement: walking, stairs, light stretches, active breaks. No performance pressure.",
|
| 33 |
+
"cerveau": "Gentle stimulation: curiosity, breathing, tiny games, small learning moments. No medical jargon.",
|
| 34 |
+
"liens": "Simple connections: gratitude, short texts, quick calls, shared moments. Warm and inclusive tone.",
|
| 35 |
+
"bien-etre": "Micro well-being: breaks, sleep rhythm, breathing, tiny soothing rituals. Never guilt-based.",
|
| 36 |
},
|
| 37 |
}
|
| 38 |
|
| 39 |
+
# Few-shot examples: now 4 questions each
|
| 40 |
FEWSHOTS = {
|
| 41 |
"fr": {
|
| 42 |
"alimentation": {
|
|
|
|
| 44 |
"Quelle boisson te donne envie de boire plus d’eau dans la journée ?",
|
| 45 |
"Quel ajout simple rend ton petit-déj plus rassasiant ?",
|
| 46 |
"Quand as-tu naturellement faim d’un fruit ou d’un yaourt ?",
|
| 47 |
+
"Quelle petite habitude t’aide à ne pas sauter de repas ?",
|
| 48 |
],
|
| 49 |
"micro_actions": [
|
| 50 |
"Remplir une gourde ce matin.",
|
|
|
|
| 56 |
"Quel trajet pourrais-tu faire à pied au moins une fois cette semaine ?",
|
| 57 |
"Quelle pause-active de 2 minutes peux-tu glisser entre deux tâches ?",
|
| 58 |
"Qu’est-ce qui te fait bouger sans y penser (ex: marcher au téléphone) ?",
|
| 59 |
+
"Quel moment conviendrait pour quelques étirements doux chaque jour ?",
|
| 60 |
],
|
| 61 |
"micro_actions": [
|
| 62 |
"Monter un étage par les escaliers aujourd’hui.",
|
|
|
|
| 68 |
"Qu’est-ce qui a suscité ta curiosité aujourd’hui ?",
|
| 69 |
"Quel moment t’irait pour 3 minutes de respiration ?",
|
| 70 |
"Quel mini-jeu aimes-tu pour réveiller l’esprit (ex: 3 mots fléchés) ?",
|
| 71 |
+
"Quel petit sujet aimerais-tu explorer cette semaine ?",
|
| 72 |
],
|
| 73 |
"micro_actions": [
|
| 74 |
"Programmer un minuteur de 3 minutes pour respirer.",
|
|
|
|
| 80 |
"Qui pourrais-tu remercier aujourd’hui et comment ?",
|
| 81 |
"À qui enverrais-tu un message court pour reprendre contact ?",
|
| 82 |
"Avec qui partagerais-tu une courte marche cette semaine ?",
|
| 83 |
+
"Avec qui aimerais-tu avoir une vraie conversation bientôt ?",
|
| 84 |
],
|
| 85 |
"micro_actions": [
|
| 86 |
"Envoyer un message de gratitude à une personne.",
|
|
|
|
| 92 |
"Quel signal t’indique qu’il est temps de faire une pause ?",
|
| 93 |
"Quelle routine de 2 minutes t’aide à te recentrer ?",
|
| 94 |
"Quel moment favorise un coucher plus régulier ?",
|
| 95 |
+
"Qu’est-ce qui t’aide à te sentir plus léger·e en fin de journée ?",
|
| 96 |
],
|
| 97 |
"micro_actions": [
|
| 98 |
"Éteindre les écrans 10 minutes plus tôt ce soir.",
|
|
|
|
| 106 |
"What drink makes you want to sip more water through the day?",
|
| 107 |
"What small add-on makes your breakfast more filling?",
|
| 108 |
"When do you naturally crave a fruit or yogurt?",
|
| 109 |
+
"What tiny habit helps you not skip meals?",
|
| 110 |
],
|
| 111 |
"micro_actions": [
|
| 112 |
"Fill a water bottle this morning.",
|
|
|
|
| 118 |
"Which short trip could you walk at least once this week?",
|
| 119 |
"Which 2-minute active break fits between two tasks?",
|
| 120 |
"What makes you move without noticing (e.g., walking on calls)?",
|
| 121 |
+
"When would a short stretch break feel good each day?",
|
| 122 |
],
|
| 123 |
"micro_actions": [
|
| 124 |
"Take one flight of stairs today.",
|
|
|
|
| 130 |
"What sparked your curiosity today?",
|
| 131 |
"When could you do 3 minutes of breathing?",
|
| 132 |
"Which mini-game wakes you up (e.g., 3 crossword clues)?",
|
| 133 |
+
"What small topic would you like to learn about this week?",
|
| 134 |
],
|
| 135 |
"micro_actions": [
|
| 136 |
"Set a 3-minute timer to breathe.",
|
|
|
|
| 142 |
"Who could you thank today—and how?",
|
| 143 |
"Who might you text briefly to reconnect?",
|
| 144 |
"Who could you invite for a short walk this week?",
|
| 145 |
+
"Who would you like to have a real conversation with soon?",
|
| 146 |
],
|
| 147 |
"micro_actions": [
|
| 148 |
"Send a gratitude message to one person.",
|
|
|
|
| 154 |
"What cue tells you it’s time for a pause?",
|
| 155 |
"What 2-minute routine helps you reset?",
|
| 156 |
"What time supports a steadier bedtime?",
|
| 157 |
+
"What helps you feel lighter at the end of the day?",
|
| 158 |
],
|
| 159 |
"micro_actions": [
|
| 160 |
"Turn screens off 10 minutes earlier tonight.",
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 168 |
+
# PROMPT & MODEL
|
| 169 |
|
| 170 |
|
| 171 |
def build_prompt(lang: str, category_key: str, variant: str) -> str:
|
|
|
|
| 178 |
few = FEWSHOTS[lang][category_key]
|
| 179 |
|
| 180 |
variant_fr = (
|
| 181 |
+
"Ton: ludique et original (‘meilleur’)."
|
| 182 |
if variant == "best"
|
| 183 |
+
else "Ton: introspectif et authentique (‘plus sincère’)."
|
| 184 |
)
|
| 185 |
variant_en = (
|
| 186 |
+
"Tone: playful and original (‘best’)."
|
| 187 |
if variant == "best"
|
| 188 |
+
else "Tone: introspective and authentic (‘most sincere’)."
|
| 189 |
)
|
| 190 |
|
| 191 |
+
# 4 questions now
|
| 192 |
schema = (
|
| 193 |
"{\n"
|
| 194 |
' "category": "<category_key>",\n'
|
| 195 |
' "language": "<fr|en>",\n'
|
| 196 |
+
' "questions": ["q1", "q2", "q3", "q4"],\n'
|
| 197 |
' "micro_actions": ["m1", "m2"],\n'
|
| 198 |
' "tone": "playful|sincere|ludique|sincère",\n'
|
| 199 |
' "safety_notes": "short coaching tips"\n'
|
|
|
|
| 203 |
if lang == "fr":
|
| 204 |
system = (
|
| 205 |
"Tu es l’IA du jeu de cartes Neurovie, inspiré du modèle FINGER. "
|
| 206 |
+
"Une carte = une question sur les routines du quotidien."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
)
|
| 208 |
safety = (
|
| 209 |
+
"Règles:\n"
|
| 210 |
+
"- Pas de conseils médicaux ni de diagnostics.\n"
|
| 211 |
+
"- Langage bienveillant, inclusif, concret.\n"
|
| 212 |
+
"- Phrases courtes, sans emojis.\n"
|
|
|
|
| 213 |
)
|
| 214 |
user = (
|
| 215 |
f"Catégorie: {cat['fr']} {cat['icon']}. {variant_fr}\n"
|
| 216 |
+
f"Focus: {guide}\n"
|
| 217 |
+
"Génère 4 QUESTIONS et 2 MICRO-ACTIONS, chacune en une phrase.\n"
|
|
|
|
| 218 |
f"Exemples de style (ne pas copier mot à mot): questions={few['questions']} micro_actions={few['micro_actions']}\n"
|
| 219 |
+
f"Schéma JSON strict:\n{schema}\n"
|
|
|
|
| 220 |
"RENVOIE UNIQUEMENT DU JSON VALIDE."
|
| 221 |
)
|
|
|
|
|
|
|
| 222 |
else:
|
| 223 |
system = (
|
| 224 |
"You are the AI for the Neurovie card game, inspired by the FINGER model. "
|
| 225 |
+
"One card = one question about daily routines."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
)
|
| 227 |
safety = (
|
| 228 |
+
"Rules:\n"
|
| 229 |
+
"- No medical advice or diagnosis.\n"
|
| 230 |
+
"- Kind, concrete, inclusive language.\n"
|
| 231 |
+
"- Short sentences, no emojis.\n"
|
|
|
|
| 232 |
)
|
| 233 |
user = (
|
| 234 |
f"Category: {cat['en']} {cat['icon']}. {variant_en}\n"
|
| 235 |
+
f"Focus: {guide}\n"
|
| 236 |
+
"Generate 4 QUESTIONS and 2 MICRO-ACTIONS, one sentence each.\n"
|
|
|
|
| 237 |
f"Style examples (do not copy verbatim): questions={few['questions']} micro_actions={few['micro_actions']}\n"
|
| 238 |
+
f"Strict JSON schema:\n{schema}\n"
|
|
|
|
| 239 |
"RETURN VALID JSON ONLY."
|
| 240 |
)
|
| 241 |
+
|
| 242 |
+
return f"{system}\n\n{safety}\n\n{user}"
|
| 243 |
|
| 244 |
|
| 245 |
def try_parse_json(text: str) -> Optional[Dict[str, Any]]:
|
|
|
|
| 260 |
q = [str(x).strip() for x in data.get("questions", []) if str(x).strip()]
|
| 261 |
m = [str(x).strip() for x in data.get("micro_actions", []) if str(x).strip()]
|
| 262 |
|
| 263 |
+
# exactly 4 questions, 2 micro-actions
|
| 264 |
+
q = (q + [""] * 4)[:4]
|
| 265 |
m = (m + [""] * 2)[:2]
|
| 266 |
|
| 267 |
if not data.get("tone"):
|
|
|
|
| 293 |
def model_call(prompt: str) -> str:
|
| 294 |
client = InferenceClient(model=MODEL_ID, token=HF_TOKEN)
|
| 295 |
|
|
|
|
| 296 |
try:
|
| 297 |
resp = client.chat.completions.create(
|
| 298 |
model=MODEL_ID,
|
|
|
|
| 316 |
except Exception:
|
| 317 |
pass
|
| 318 |
|
|
|
|
| 319 |
return client.text_generation(
|
| 320 |
prompt,
|
| 321 |
max_new_tokens=220,
|
|
|
|
| 325 |
).strip()
|
| 326 |
|
| 327 |
|
| 328 |
+
def generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
|
| 329 |
prompt = build_prompt(lang, category_key, variant)
|
| 330 |
|
| 331 |
raw_text = None
|
|
|
|
| 341 |
parsed = {
|
| 342 |
"category": category_key,
|
| 343 |
"language": lang,
|
| 344 |
+
"questions": few["questions"][:4],
|
| 345 |
"micro_actions": few["micro_actions"][:2],
|
| 346 |
"tone": (
|
| 347 |
"ludique"
|
|
|
|
| 361 |
|
| 362 |
normalized = normalize_output(parsed, lang, category_key, variant)
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
return {
|
| 365 |
+
"questions": normalized["questions"],
|
| 366 |
+
"micro_actions": normalized["micro_actions"],
|
| 367 |
"raw_json": json.dumps(normalized, ensure_ascii=False, indent=2),
|
| 368 |
}
|
| 369 |
|
| 370 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 371 |
+
# UI – pastel, animated, cards
|
| 372 |
+
|
| 373 |
|
| 374 |
CUSTOM_CSS = """
|
| 375 |
:root {
|
| 376 |
+
--nv-bg: #f7f2ec;
|
| 377 |
--nv-card: #fffdf8;
|
| 378 |
+
--nv-border: #e5d8c7;
|
| 379 |
+
--nv-accent: #f38a6b;
|
| 380 |
+
--nv-accent-soft: #ffe4d4;
|
| 381 |
+
--nv-accent-teal: #9fcfd1;
|
| 382 |
+
--nv-accent-lilac: #d9c7f2;
|
| 383 |
--nv-text-main: #262626;
|
| 384 |
--nv-text-muted: #6c6459;
|
| 385 |
}
|
| 386 |
|
|
|
|
| 387 |
.gradio-container {
|
| 388 |
font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
|
| 389 |
+
background: radial-gradient(circle at top left, #fff6ee 0, #f7f2ec 50%, #f2ece6 100%);
|
| 390 |
+
max-width: 960px !important;
|
| 391 |
margin: 0 auto !important;
|
| 392 |
padding: 32px 0 40px 0;
|
| 393 |
position: relative;
|
| 394 |
}
|
| 395 |
|
| 396 |
+
/* blobs */
|
| 397 |
@keyframes nvBlobFloat {
|
| 398 |
+
0% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.6; }
|
| 399 |
50% { transform: translate3d(10px, -8px, 0) scale(1.04); opacity: 0.9; }
|
| 400 |
+
100% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.6; }
|
| 401 |
}
|
|
|
|
| 402 |
.gradio-container::before,
|
| 403 |
.gradio-container::after {
|
| 404 |
content: "";
|
|
|
|
| 413 |
.gradio-container::before {
|
| 414 |
top: -80px;
|
| 415 |
left: -60px;
|
| 416 |
+
background: radial-gradient(circle at 30% 30%, #ffd5c5, transparent 60%);
|
| 417 |
}
|
| 418 |
.gradio-container::after {
|
| 419 |
bottom: -120px;
|
| 420 |
right: -40px;
|
| 421 |
+
background: radial-gradient(circle at 70% 70%, #c3e4ff, transparent 60%);
|
| 422 |
animation-delay: 4s;
|
| 423 |
}
|
| 424 |
|
| 425 |
+
/* main shell */
|
| 426 |
@keyframes nvCardIn {
|
| 427 |
0% { opacity: 0; transform: translateY(8px) scale(0.98); }
|
| 428 |
100% { opacity: 1; transform: translateY(0) scale(1); }
|
|
|
|
| 482 |
transition: background 150ms ease, transform 120ms ease, box-shadow 120ms ease;
|
| 483 |
}
|
| 484 |
.nv-pills input:checked + label {
|
| 485 |
+
background: linear-gradient(135deg, #ffe4d4, #ffd6c6) !important;
|
| 486 |
border-color: var(--nv-accent) !important;
|
| 487 |
color: var(--nv-text-main) !important;
|
| 488 |
+
box-shadow: 0 4px 10px rgba(243, 138, 107, 0.18);
|
| 489 |
transform: translateY(-1px);
|
| 490 |
}
|
| 491 |
|
| 492 |
/* button */
|
| 493 |
@keyframes nvPulse {
|
| 494 |
+
0% { transform: translateY(0) scale(1); box-shadow: 0 10px 22px rgba(243, 138, 107, 0.30); }
|
| 495 |
+
50% { transform: translateY(-1px) scale(1.02); box-shadow: 0 14px 26px rgba(243, 138, 107, 0.40); }
|
| 496 |
+
100% { transform: translateY(0) scale(1); box-shadow: 0 10px 22px rgba(243, 138, 107, 0.30); }
|
| 497 |
}
|
| 498 |
.gr-button {
|
| 499 |
border-radius: 999px !important;
|
|
|
|
| 502 |
font-size: 0.96rem !important;
|
| 503 |
border: none !important;
|
| 504 |
background:
|
| 505 |
+
radial-gradient(circle at 0 0, #ffe4d4, transparent 55%),
|
| 506 |
+
linear-gradient(135deg, #f6a37d, #f38a6b) !important;
|
| 507 |
color: #fff !important;
|
| 508 |
animation: nvPulse 4s ease-in-out infinite;
|
| 509 |
}
|
|
|
|
| 511 |
animation-duration: 1.4s;
|
| 512 |
}
|
| 513 |
|
| 514 |
+
/* cards */
|
| 515 |
+
@keyframes nvCardFloat {
|
| 516 |
+
0% { transform: translateY(0); }
|
| 517 |
+
50% { transform: translateY(-2px); }
|
| 518 |
+
100% { transform: translateY(0); }
|
| 519 |
+
}
|
| 520 |
+
.nv-card-grid {
|
| 521 |
+
display: grid;
|
| 522 |
+
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
| 523 |
+
gap: 10px;
|
| 524 |
}
|
| 525 |
+
.nv-card {
|
| 526 |
+
background: #ffffff;
|
| 527 |
border-radius: 18px;
|
| 528 |
+
padding: 10px 11px;
|
| 529 |
+
font-size: 0.9rem;
|
| 530 |
+
border: 1px solid rgba(0,0,0,0.03);
|
| 531 |
+
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.06);
|
| 532 |
+
animation: nvCardFloat 6s ease-in-out infinite;
|
| 533 |
+
}
|
| 534 |
+
.nv-card[data-kind="question"] {
|
| 535 |
+
background: radial-gradient(circle at 0 0, #f9ecff 0, #ffffff 60%);
|
| 536 |
+
border-color: #e0d2f5;
|
| 537 |
+
}
|
| 538 |
+
.nv-card[data-kind="micro"] {
|
| 539 |
+
background: radial-gradient(circle at 0 0, #e6f7f8 0, #ffffff 60%);
|
| 540 |
+
border-color: #cfe6e6;
|
| 541 |
+
}
|
| 542 |
+
.nv-card-title {
|
| 543 |
+
font-size: 0.72rem;
|
| 544 |
+
letter-spacing: 0.14em;
|
| 545 |
+
text-transform: uppercase;
|
| 546 |
+
color: #9a8fb6;
|
| 547 |
+
margin-bottom: 4px;
|
| 548 |
}
|
| 549 |
|
| 550 |
+
/* JSON box */
|
| 551 |
.nv-json code, .nv-json textarea {
|
| 552 |
border-radius: 16px !important;
|
| 553 |
}
|
|
|
|
| 578 |
def click_handler(l, c, v):
|
| 579 |
c_key = _map_category(c)
|
| 580 |
out = generate(l, c_key, v)
|
| 581 |
+
qs = out["questions"]
|
| 582 |
+
ms = out["micro_actions"]
|
| 583 |
+
# safely index
|
| 584 |
+
q_cards = [qs[i] if i < len(qs) else "" for i in range(4)]
|
| 585 |
+
m_cards = [ms[i] if i < len(ms) else "" for i in range(2)]
|
| 586 |
+
return (*q_cards, *m_cards, out["raw_json"])
|
| 587 |
|
| 588 |
|
| 589 |
with gr.Blocks(title="Neurovie – Question Studio") as demo:
|
|
|
|
| 590 |
gr.HTML(f"<style>{CUSTOM_CSS}</style>")
|
| 591 |
|
| 592 |
with gr.Column(elem_classes="nv-shell"):
|
|
|
|
| 596 |
<div class="nv-badge">NEUROVIE · FINGER</div>
|
| 597 |
<div class="nv-title">Question Studio</div>
|
| 598 |
<div class="nv-subtitle">
|
| 599 |
+
Minimal prompts for rich conversations — draw 4 questions and 2 micro-actions.
|
| 600 |
</div>
|
| 601 |
</div>
|
| 602 |
"""
|
| 603 |
)
|
| 604 |
|
| 605 |
+
# Settings
|
| 606 |
with gr.Row(elem_classes="nv-section"):
|
| 607 |
with gr.Column():
|
| 608 |
gr.HTML("<div class='nv-label'>Language</div>")
|
|
|
|
| 638 |
|
| 639 |
btn = gr.Button("Generate card set ✨")
|
| 640 |
|
| 641 |
+
# Preview cards
|
| 642 |
+
with gr.Row(elem_classes="nv-section"):
|
| 643 |
+
with gr.Column():
|
| 644 |
+
gr.HTML("<div class='nv-label'>Questions</div>")
|
| 645 |
+
q1 = gr.HTML("<div class='nv-card' data-kind='question'><div class='nv-card-title'>Question 1</div><div></div></div>")
|
| 646 |
+
q2 = gr.HTML("<div class='nv-card' data-kind='question'><div class='nv-card-title'>Question 2</div><div></div></div>")
|
| 647 |
+
q3 = gr.HTML("<div class='nv-card' data-kind='question'><div class='nv-card-title'>Question 3</div><div></div></div>")
|
| 648 |
+
q4 = gr.HTML("<div class='nv-card' data-kind='question'><div class='nv-card-title'>Question 4</div><div></div></div>")
|
| 649 |
+
# wrap them in a grid via simple container
|
| 650 |
+
gr.HTML(
|
| 651 |
+
"""
|
| 652 |
+
<script>
|
| 653 |
+
// no-op: cards already styled; JS kept minimal
|
| 654 |
+
</script>
|
| 655 |
+
"""
|
| 656 |
+
)
|
| 657 |
+
with gr.Column():
|
| 658 |
+
gr.HTML("<div class='nv-label'>Micro-actions</div>")
|
| 659 |
+
m1 = gr.HTML("<div class='nv-card' data-kind='micro'><div class='nv-card-title'>Micro-action 1</div><div></div></div>")
|
| 660 |
+
m2 = gr.HTML("<div class='nv-card' data-kind='micro'><div class='nv-card-title'>Micro-action 2</div><div></div></div>")
|
| 661 |
|
| 662 |
+
# JSON output (for dev)
|
| 663 |
with gr.Column(elem_classes=["nv-section", "nv-json"]):
|
| 664 |
gr.HTML("<div class='nv-label'>JSON (for dev)</div>")
|
| 665 |
raw_json = gr.Code(
|
|
|
|
| 668 |
show_label=False,
|
| 669 |
)
|
| 670 |
|
| 671 |
+
# we update the inner HTML of the cards using a small template in Python
|
| 672 |
+
def update_cards(l, c, v):
|
| 673 |
+
texts = click_handler(l, c, v)
|
| 674 |
+
qs = texts[:4]
|
| 675 |
+
ms = texts[4:6]
|
| 676 |
+
json_str = texts[6]
|
| 677 |
+
|
| 678 |
+
def card_html(kind, title, body):
|
| 679 |
+
kind_attr = "question" if kind == "q" else "micro"
|
| 680 |
+
return f"<div class='nv-card' data-kind='{kind_attr}'><div class='nv-card-title'>{title}</div><div>{body}</div></div>"
|
| 681 |
+
|
| 682 |
+
return (
|
| 683 |
+
card_html("q", "Question 1", qs[0]),
|
| 684 |
+
card_html("q", "Question 2", qs[1]),
|
| 685 |
+
card_html("q", "Question 3", qs[2]),
|
| 686 |
+
card_html("q", "Question 4", qs[3]),
|
| 687 |
+
card_html("m", "Micro-action 1", ms[0]),
|
| 688 |
+
card_html("m", "Micro-action 2", ms[1]),
|
| 689 |
+
json_str,
|
| 690 |
+
)
|
| 691 |
+
|
| 692 |
+
btn.click(
|
| 693 |
+
update_cards,
|
| 694 |
+
[lang, category, variant],
|
| 695 |
+
[q1, q2, q3, q4, m1, m2, raw_json],
|
| 696 |
+
)
|
| 697 |
|
| 698 |
if __name__ == "__main__":
|
| 699 |
demo.launch()
|