Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import os
|
| 2 |
import re
|
| 3 |
import json
|
|
|
|
| 4 |
from typing import Dict, Any, List, Optional
|
| 5 |
|
| 6 |
import gradio as gr
|
|
@@ -8,8 +9,9 @@ from huggingface_hub import InferenceClient
|
|
| 8 |
|
| 9 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 10 |
# CONFIG
|
|
|
|
| 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
|
| 13 |
|
| 14 |
CATEGORIES = [
|
| 15 |
{"key": "alimentation", "icon": "🍎", "fr": "Alimentation", "en": "Nutrition"},
|
|
@@ -36,7 +38,7 @@ GUIDES = {
|
|
| 36 |
},
|
| 37 |
}
|
| 38 |
|
| 39 |
-
# Few-shot examples
|
| 40 |
FEWSHOTS = {
|
| 41 |
"fr": {
|
| 42 |
"alimentation": {
|
|
@@ -45,10 +47,14 @@ FEWSHOTS = {
|
|
| 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.",
|
| 51 |
"Ajouter un fruit à la collation de l’après-midi.",
|
|
|
|
|
|
|
| 52 |
],
|
| 53 |
},
|
| 54 |
"mouvement": {
|
|
@@ -57,10 +63,14 @@ FEWSHOTS = {
|
|
| 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.",
|
| 63 |
"Faire 5 étirements doux après le café.",
|
|
|
|
|
|
|
| 64 |
],
|
| 65 |
},
|
| 66 |
"cerveau": {
|
|
@@ -69,10 +79,14 @@ FEWSHOTS = {
|
|
| 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.",
|
| 75 |
"Lire un paragraphe d’un sujet nouveau ce soir.",
|
|
|
|
|
|
|
| 76 |
],
|
| 77 |
},
|
| 78 |
"liens": {
|
|
@@ -81,10 +95,14 @@ FEWSHOTS = {
|
|
| 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.",
|
| 87 |
"Proposer une pause-café de 10 minutes.",
|
|
|
|
|
|
|
| 88 |
],
|
| 89 |
},
|
| 90 |
"bien-etre": {
|
|
@@ -93,10 +111,14 @@ FEWSHOTS = {
|
|
| 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.",
|
| 99 |
"Écrire 3 lignes sur ton humeur du jour.",
|
|
|
|
|
|
|
| 100 |
],
|
| 101 |
},
|
| 102 |
},
|
|
@@ -107,10 +129,14 @@ FEWSHOTS = {
|
|
| 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.",
|
| 113 |
"Add one fruit to your afternoon snack.",
|
|
|
|
|
|
|
| 114 |
],
|
| 115 |
},
|
| 116 |
"mouvement": {
|
|
@@ -119,10 +145,14 @@ FEWSHOTS = {
|
|
| 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.",
|
| 125 |
"Do 5 light stretches after coffee.",
|
|
|
|
|
|
|
| 126 |
],
|
| 127 |
},
|
| 128 |
"cerveau": {
|
|
@@ -131,10 +161,14 @@ FEWSHOTS = {
|
|
| 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.",
|
| 137 |
"Read one paragraph on a new topic tonight.",
|
|
|
|
|
|
|
| 138 |
],
|
| 139 |
},
|
| 140 |
"liens": {
|
|
@@ -143,10 +177,14 @@ FEWSHOTS = {
|
|
| 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.",
|
| 149 |
"Offer a 10-minute coffee break.",
|
|
|
|
|
|
|
| 150 |
],
|
| 151 |
},
|
| 152 |
"bien-etre": {
|
|
@@ -155,17 +193,21 @@ FEWSHOTS = {
|
|
| 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.",
|
| 161 |
"Write three lines about your mood today.",
|
|
|
|
|
|
|
| 162 |
],
|
| 163 |
},
|
| 164 |
},
|
| 165 |
}
|
| 166 |
|
| 167 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 168 |
-
# PROMPT
|
| 169 |
|
| 170 |
|
| 171 |
def build_prompt(lang: str, category_key: str, variant: str) -> str:
|
|
@@ -177,18 +219,13 @@ def build_prompt(lang: str, category_key: str, variant: str) -> str:
|
|
| 177 |
guide = GUIDES[lang][category_key]
|
| 178 |
few = FEWSHOTS[lang][category_key]
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 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'
|
|
@@ -196,50 +233,55 @@ def build_prompt(lang: str, category_key: str, variant: str) -> str:
|
|
| 196 |
' "questions": ["q1", "q2", "q3", "q4"],\n'
|
| 197 |
' "micro_actions": ["m1", "m2"],\n'
|
| 198 |
' "tone": "playful|sincere|ludique|sincère",\n'
|
| 199 |
-
' "safety_notes": "
|
| 200 |
"}"
|
| 201 |
)
|
| 202 |
|
|
|
|
|
|
|
|
|
|
| 203 |
if lang == "fr":
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
|
|
|
|
|
|
| 222 |
else:
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
|
| 245 |
def try_parse_json(text: str) -> Optional[Dict[str, Any]]:
|
|
@@ -254,52 +296,17 @@ def try_parse_json(text: str) -> Optional[Dict[str, Any]]:
|
|
| 254 |
return None
|
| 255 |
|
| 256 |
|
| 257 |
-
def normalize_output(
|
| 258 |
-
data: Dict[str, Any], lang: str, category_key: str, variant: str
|
| 259 |
-
) -> 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"):
|
| 268 |
-
if lang == "fr":
|
| 269 |
-
tone = "ludique" if variant == "best" else "sincère"
|
| 270 |
-
else:
|
| 271 |
-
tone = "playful" if variant == "best" else "sincere"
|
| 272 |
-
else:
|
| 273 |
-
tone = str(data["tone"])
|
| 274 |
-
|
| 275 |
-
safety_notes = data.get("safety_notes")
|
| 276 |
-
if not safety_notes:
|
| 277 |
-
safety_notes = (
|
| 278 |
-
"Reste bienveillant·e, évite les conseils médicaux."
|
| 279 |
-
if lang == "fr"
|
| 280 |
-
else "Be kind, avoid medical advice."
|
| 281 |
-
)
|
| 282 |
-
|
| 283 |
-
return {
|
| 284 |
-
"category": category_key,
|
| 285 |
-
"language": lang,
|
| 286 |
-
"questions": q,
|
| 287 |
-
"micro_actions": m,
|
| 288 |
-
"tone": tone,
|
| 289 |
-
"safety_notes": safety_notes,
|
| 290 |
-
}
|
| 291 |
-
|
| 292 |
-
|
| 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,
|
| 299 |
messages=[{"role": "user", "content": prompt}],
|
| 300 |
-
temperature=0.
|
| 301 |
top_p=0.92,
|
| 302 |
-
max_tokens=
|
| 303 |
)
|
| 304 |
choice = resp.choices[0]
|
| 305 |
content = (
|
|
@@ -316,16 +323,45 @@ def model_call(prompt: str) -> str:
|
|
| 316 |
except Exception:
|
| 317 |
pass
|
| 318 |
|
|
|
|
| 319 |
return client.text_generation(
|
| 320 |
prompt,
|
| 321 |
-
max_new_tokens=
|
| 322 |
-
temperature=0.
|
| 323 |
top_p=0.92,
|
| 324 |
return_full_text=False,
|
| 325 |
).strip()
|
| 326 |
|
| 327 |
|
| 328 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
prompt = build_prompt(lang, category_key, variant)
|
| 330 |
|
| 331 |
raw_text = None
|
|
@@ -336,31 +372,24 @@ def generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
|
|
| 336 |
|
| 337 |
parsed = try_parse_json(raw_text) if raw_text else None
|
| 338 |
|
| 339 |
-
if
|
|
|
|
|
|
|
|
|
|
| 340 |
few = FEWSHOTS[lang][category_key]
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
"category": category_key,
|
| 343 |
"language": lang,
|
| 344 |
-
"questions":
|
| 345 |
-
"micro_actions":
|
| 346 |
-
"tone":
|
| 347 |
-
|
| 348 |
-
if (lang == "fr" and variant == "best")
|
| 349 |
-
else "sincère"
|
| 350 |
-
if lang == "fr"
|
| 351 |
-
else "playful"
|
| 352 |
-
if variant == "best"
|
| 353 |
-
else "sincere"
|
| 354 |
-
),
|
| 355 |
-
"safety_notes": (
|
| 356 |
-
"Reste bienveillant·e, évite les conseils médicaux."
|
| 357 |
-
if lang == "fr"
|
| 358 |
-
else "Be kind, avoid medical advice."
|
| 359 |
-
),
|
| 360 |
}
|
| 361 |
|
| 362 |
-
normalized = normalize_output(parsed, lang, category_key, variant)
|
| 363 |
-
|
| 364 |
return {
|
| 365 |
"questions": normalized["questions"],
|
| 366 |
"micro_actions": normalized["micro_actions"],
|
|
@@ -368,7 +397,7 @@ def generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
|
|
| 368 |
}
|
| 369 |
|
| 370 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 371 |
-
# UI – pastel, animated,
|
| 372 |
|
| 373 |
|
| 374 |
CUSTOM_CSS = """
|
|
@@ -379,7 +408,6 @@ CUSTOM_CSS = """
|
|
| 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 |
}
|
|
@@ -393,7 +421,7 @@ CUSTOM_CSS = """
|
|
| 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; }
|
|
@@ -564,6 +592,7 @@ CUSTOM_CSS = """
|
|
| 564 |
}
|
| 565 |
"""
|
| 566 |
|
|
|
|
| 567 |
def _map_category(choice: str) -> str:
|
| 568 |
mapping = {
|
| 569 |
"alimentation 🍎": "alimentation",
|
|
@@ -575,16 +604,24 @@ def _map_category(choice: str) -> str:
|
|
| 575 |
return mapping.get(choice, "alimentation")
|
| 576 |
|
| 577 |
|
| 578 |
-
def
|
| 579 |
-
|
| 580 |
-
out =
|
| 581 |
qs = out["questions"]
|
| 582 |
ms = out["micro_actions"]
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>")
|
|
@@ -638,26 +675,20 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
|
|
| 638 |
|
| 639 |
btn = gr.Button("Generate card set ✨")
|
| 640 |
|
| 641 |
-
#
|
| 642 |
with gr.Row(elem_classes="nv-section"):
|
| 643 |
with gr.Column():
|
| 644 |
gr.HTML("<div class='nv-label'>Questions</div>")
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 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 |
-
|
| 660 |
-
|
|
|
|
| 661 |
|
| 662 |
# JSON output (for dev)
|
| 663 |
with gr.Column(elem_classes=["nv-section", "nv-json"]):
|
|
@@ -668,27 +699,6 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
|
|
| 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],
|
|
|
|
| 1 |
import os
|
| 2 |
import re
|
| 3 |
import json
|
| 4 |
+
import random
|
| 5 |
from typing import Dict, Any, List, Optional
|
| 6 |
|
| 7 |
import gradio as gr
|
|
|
|
| 9 |
|
| 10 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 11 |
# CONFIG
|
| 12 |
+
|
| 13 |
MODEL_ID = os.environ.get("MODEL_ID", "meta-llama/Meta-Llama-3.1-8B-Instruct")
|
| 14 |
+
HF_TOKEN = os.environ.get("HF_TOKEN") # set as repo secret if model needs auth
|
| 15 |
|
| 16 |
CATEGORIES = [
|
| 17 |
{"key": "alimentation", "icon": "🍎", "fr": "Alimentation", "en": "Nutrition"},
|
|
|
|
| 38 |
},
|
| 39 |
}
|
| 40 |
|
| 41 |
+
# Few-shot examples for style + fallback
|
| 42 |
FEWSHOTS = {
|
| 43 |
"fr": {
|
| 44 |
"alimentation": {
|
|
|
|
| 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 |
"Quelle petite habitude t’aide à ne pas sauter de repas ?",
|
| 50 |
+
"Quel plat simple te fait du bien après une journée chargée ?",
|
| 51 |
+
"Quelle collation t’aide à tenir jusqu’au dîner sans avoir trop faim ?",
|
| 52 |
],
|
| 53 |
"micro_actions": [
|
| 54 |
"Remplir une gourde ce matin.",
|
| 55 |
"Ajouter un fruit à la collation de l’après-midi.",
|
| 56 |
+
"Remplacer une boisson sucrée par un verre d’eau aujourd’hui.",
|
| 57 |
+
"Préparer un snack simple pour demain.",
|
| 58 |
],
|
| 59 |
},
|
| 60 |
"mouvement": {
|
|
|
|
| 63 |
"Quelle pause-active de 2 minutes peux-tu glisser entre deux tâches ?",
|
| 64 |
"Qu’est-ce qui te fait bouger sans y penser (ex: marcher au téléphone) ?",
|
| 65 |
"Quel moment conviendrait pour quelques étirements doux chaque jour ?",
|
| 66 |
+
"Avec qui aimerais-tu partager une courte marche ?",
|
| 67 |
+
"Quel geste te réveille le matin (étirement, marche, danse exprès…) ?",
|
| 68 |
],
|
| 69 |
"micro_actions": [
|
| 70 |
"Monter un étage par les escaliers aujourd’hui.",
|
| 71 |
"Faire 5 étirements doux après le café.",
|
| 72 |
+
"Se lever pendant un appel et marcher quelques pas.",
|
| 73 |
+
"Programmer une mini-alarme « bouger » dans l’après-midi.",
|
| 74 |
],
|
| 75 |
},
|
| 76 |
"cerveau": {
|
|
|
|
| 79 |
"Quel moment t’irait pour 3 minutes de respiration ?",
|
| 80 |
"Quel mini-jeu aimes-tu pour réveiller l’esprit (ex: 3 mots fléchés) ?",
|
| 81 |
"Quel petit sujet aimerais-tu explorer cette semaine ?",
|
| 82 |
+
"Quelle activité calme t’aide à passer du travail au repos ?",
|
| 83 |
+
"Quel souvenir récent t’a fait sourire en y repensant ?",
|
| 84 |
],
|
| 85 |
"micro_actions": [
|
| 86 |
"Programmer un minuteur de 3 minutes pour respirer.",
|
| 87 |
"Lire un paragraphe d’un sujet nouveau ce soir.",
|
| 88 |
+
"Faire un mini-jeu de cerveau (3 mots fléchés, Sudoku, etc.).",
|
| 89 |
+
"Noter une idée ou question qui t’intrigue.",
|
| 90 |
],
|
| 91 |
},
|
| 92 |
"liens": {
|
|
|
|
| 95 |
"À qui enverrais-tu un message court pour reprendre contact ?",
|
| 96 |
"Avec qui partagerais-tu une courte marche cette semaine ?",
|
| 97 |
"Avec qui aimerais-tu avoir une vraie conversation bientôt ?",
|
| 98 |
+
"Quand t’es-tu senti·e soutenu·e pour la dernière fois, et par qui ?",
|
| 99 |
+
"Qui aimerais-tu encourager cette semaine ?",
|
| 100 |
],
|
| 101 |
"micro_actions": [
|
| 102 |
"Envoyer un message de gratitude à une personne.",
|
| 103 |
"Proposer une pause-café de 10 minutes.",
|
| 104 |
+
"Envoyer une photo ou un souvenir à quelqu’un avec un petit mot.",
|
| 105 |
+
"Poser une vraie question à quelqu’un sur sa journée.",
|
| 106 |
],
|
| 107 |
},
|
| 108 |
"bien-etre": {
|
|
|
|
| 111 |
"Quelle routine de 2 minutes t’aide à te recentrer ?",
|
| 112 |
"Quel moment favorise un coucher plus régulier ?",
|
| 113 |
"Qu’est-ce qui t’aide à te sentir plus léger·e en fin de journée ?",
|
| 114 |
+
"Quel endroit de ton quotidien te donne une sensation de calme ?",
|
| 115 |
+
"Quand as-tu l’impression de vraiment respirer ?",
|
| 116 |
],
|
| 117 |
"micro_actions": [
|
| 118 |
"Éteindre les écrans 10 minutes plus tôt ce soir.",
|
| 119 |
"Écrire 3 lignes sur ton humeur du jour.",
|
| 120 |
+
"Prendre 5 respirations lentes avant de changer d’activité.",
|
| 121 |
+
"Planifier une mini-pause de 5 minutes pour toi demain.",
|
| 122 |
],
|
| 123 |
},
|
| 124 |
},
|
|
|
|
| 129 |
"What small add-on makes your breakfast more filling?",
|
| 130 |
"When do you naturally crave a fruit or yogurt?",
|
| 131 |
"What tiny habit helps you not skip meals?",
|
| 132 |
+
"What simple dinner feels gentle after a long day?",
|
| 133 |
+
"Which snack helps you stay focused without a big energy crash?",
|
| 134 |
],
|
| 135 |
"micro_actions": [
|
| 136 |
"Fill a water bottle this morning.",
|
| 137 |
"Add one fruit to your afternoon snack.",
|
| 138 |
+
"Swap one sugary drink for water today.",
|
| 139 |
+
"Plan a simple snack for tomorrow.",
|
| 140 |
],
|
| 141 |
},
|
| 142 |
"mouvement": {
|
|
|
|
| 145 |
"Which 2-minute active break fits between two tasks?",
|
| 146 |
"What makes you move without noticing (e.g., walking on calls)?",
|
| 147 |
"When would a short stretch break feel good each day?",
|
| 148 |
+
"Where do you naturally end up walking more?",
|
| 149 |
+
"What small move helps you wake up in the morning?",
|
| 150 |
],
|
| 151 |
"micro_actions": [
|
| 152 |
"Take one flight of stairs today.",
|
| 153 |
"Do 5 light stretches after coffee.",
|
| 154 |
+
"Stand up and walk during one call.",
|
| 155 |
+
"Set a tiny “move” reminder for this afternoon.",
|
| 156 |
],
|
| 157 |
},
|
| 158 |
"cerveau": {
|
|
|
|
| 161 |
"When could you do 3 minutes of breathing?",
|
| 162 |
"Which mini-game wakes you up (e.g., 3 crossword clues)?",
|
| 163 |
"What small topic would you like to learn about this week?",
|
| 164 |
+
"What gentle activity helps you shift from work to rest?",
|
| 165 |
+
"What recent memory made you smile when you thought of it again?",
|
| 166 |
],
|
| 167 |
"micro_actions": [
|
| 168 |
"Set a 3-minute timer to breathe.",
|
| 169 |
"Read one paragraph on a new topic tonight.",
|
| 170 |
+
"Play a tiny brain game.",
|
| 171 |
+
"Write down one idea or question that interests you.",
|
| 172 |
],
|
| 173 |
},
|
| 174 |
"liens": {
|
|
|
|
| 177 |
"Who might you text briefly to reconnect?",
|
| 178 |
"Who could you invite for a short walk this week?",
|
| 179 |
"Who would you like to have a real conversation with soon?",
|
| 180 |
+
"When did you last feel supported, and by whom?",
|
| 181 |
+
"Who would you like to encourage this week?",
|
| 182 |
],
|
| 183 |
"micro_actions": [
|
| 184 |
"Send a gratitude message to one person.",
|
| 185 |
"Offer a 10-minute coffee break.",
|
| 186 |
+
"Send a photo or memory to someone with a short note.",
|
| 187 |
+
"Ask someone one genuine question about their day.",
|
| 188 |
],
|
| 189 |
},
|
| 190 |
"bien-etre": {
|
|
|
|
| 193 |
"What 2-minute routine helps you reset?",
|
| 194 |
"What time supports a steadier bedtime?",
|
| 195 |
"What helps you feel lighter at the end of the day?",
|
| 196 |
+
"Which place in your daily life feels calming?",
|
| 197 |
+
"When do you feel like you can really breathe?",
|
| 198 |
],
|
| 199 |
"micro_actions": [
|
| 200 |
"Turn screens off 10 minutes earlier tonight.",
|
| 201 |
"Write three lines about your mood today.",
|
| 202 |
+
"Take 5 slow breaths before changing tasks.",
|
| 203 |
+
"Schedule a 5-minute mini-break for yourself tomorrow.",
|
| 204 |
],
|
| 205 |
},
|
| 206 |
},
|
| 207 |
}
|
| 208 |
|
| 209 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 210 |
+
# PROMPT + MODEL HELPERS
|
| 211 |
|
| 212 |
|
| 213 |
def build_prompt(lang: str, category_key: str, variant: str) -> str:
|
|
|
|
| 219 |
guide = GUIDES[lang][category_key]
|
| 220 |
few = FEWSHOTS[lang][category_key]
|
| 221 |
|
| 222 |
+
if variant == "best":
|
| 223 |
+
tone_fr = "ludique, original, léger"
|
| 224 |
+
tone_en = "playful, original, light"
|
| 225 |
+
else:
|
| 226 |
+
tone_fr = "sincère, introspectif, doux"
|
| 227 |
+
tone_en = "sincere, introspective, gentle"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
|
|
|
| 229 |
schema = (
|
| 230 |
"{\n"
|
| 231 |
' "category": "<category_key>",\n'
|
|
|
|
| 233 |
' "questions": ["q1", "q2", "q3", "q4"],\n'
|
| 234 |
' "micro_actions": ["m1", "m2"],\n'
|
| 235 |
' "tone": "playful|sincere|ludique|sincère",\n'
|
| 236 |
+
' "safety_notes": ""\n'
|
| 237 |
"}"
|
| 238 |
)
|
| 239 |
|
| 240 |
+
example_questions = few["questions"][:2]
|
| 241 |
+
example_micro = few["micro_actions"][:2]
|
| 242 |
+
|
| 243 |
if lang == "fr":
|
| 244 |
+
return f"""
|
| 245 |
+
Tu es l’IA du jeu de cartes Neurovie (modèle FINGER).
|
| 246 |
+
Tu crées des cartes-question pour parler de routines du quotidien.
|
| 247 |
+
|
| 248 |
+
- Catégorie: {cat['fr']} {cat['icon']}.
|
| 249 |
+
- Focus: {guide}
|
| 250 |
+
- Ton: {tone_fr}
|
| 251 |
+
- Forme: 4 questions + 2 micro-actions, 1 phrase courte chacune.
|
| 252 |
+
- Style: concret, bienveillant, sans jugement.
|
| 253 |
+
- Interdit: conseils médicaux, diagnostics, emojis.
|
| 254 |
+
|
| 255 |
+
Réponds UNIQUEMENT en JSON, sans texte autour, selon ce schéma:
|
| 256 |
+
{schema}
|
| 257 |
+
|
| 258 |
+
Exemple de style (à VARIER, ne pas copier):
|
| 259 |
+
Questions: {example_questions}
|
| 260 |
+
Micro-actions: {example_micro}
|
| 261 |
+
|
| 262 |
+
Maintenant, renvoie un NOUVEAU JSON différent de l'exemple.
|
| 263 |
+
""".strip()
|
| 264 |
else:
|
| 265 |
+
return f"""
|
| 266 |
+
You are the AI for the Neurovie card game (FINGER model).
|
| 267 |
+
You create question-cards about everyday routines.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
+
- Category: {cat['en']} {cat['icon']}.
|
| 270 |
+
- Focus: {guide}
|
| 271 |
+
- Tone: {tone_en}
|
| 272 |
+
- Format: 4 questions + 2 micro-actions, 1 short sentence each.
|
| 273 |
+
- Style: concrete, kind, non-judgmental.
|
| 274 |
+
- Forbidden: medical advice, diagnoses, emojis.
|
| 275 |
+
|
| 276 |
+
Reply ONLY with JSON, no extra text, in this shape:
|
| 277 |
+
{schema}
|
| 278 |
+
|
| 279 |
+
Style example (to vary, do NOT copy):
|
| 280 |
+
Questions: {example_questions}
|
| 281 |
+
Micro-actions: {example_micro}
|
| 282 |
+
|
| 283 |
+
Now return a NEW JSON different from the example.
|
| 284 |
+
""".strip()
|
| 285 |
|
| 286 |
|
| 287 |
def try_parse_json(text: str) -> Optional[Dict[str, Any]]:
|
|
|
|
| 296 |
return None
|
| 297 |
|
| 298 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
def model_call(prompt: str) -> str:
|
| 300 |
client = InferenceClient(model=MODEL_ID, token=HF_TOKEN)
|
| 301 |
|
| 302 |
+
# Try chat-style first
|
| 303 |
try:
|
| 304 |
resp = client.chat.completions.create(
|
| 305 |
model=MODEL_ID,
|
| 306 |
messages=[{"role": "user", "content": prompt}],
|
| 307 |
+
temperature=0.9,
|
| 308 |
top_p=0.92,
|
| 309 |
+
max_tokens=260,
|
| 310 |
)
|
| 311 |
choice = resp.choices[0]
|
| 312 |
content = (
|
|
|
|
| 323 |
except Exception:
|
| 324 |
pass
|
| 325 |
|
| 326 |
+
# Fallback: text_generation
|
| 327 |
return client.text_generation(
|
| 328 |
prompt,
|
| 329 |
+
max_new_tokens=260,
|
| 330 |
+
temperature=0.9,
|
| 331 |
top_p=0.92,
|
| 332 |
return_full_text=False,
|
| 333 |
).strip()
|
| 334 |
|
| 335 |
|
| 336 |
+
def normalize_output(
|
| 337 |
+
data: Dict[str, Any], lang: str, category_key: str, variant: str
|
| 338 |
+
) -> Dict[str, Any]:
|
| 339 |
+
q = [str(x).strip() for x in data.get("questions", []) if str(x).strip()]
|
| 340 |
+
m = [str(x).strip() for x in data.get("micro_actions", []) if str(x).strip()]
|
| 341 |
+
|
| 342 |
+
# exactly 4 questions, 2 micro-actions
|
| 343 |
+
q = (q + [""] * 4)[:4]
|
| 344 |
+
m = (m + [""] * 2)[:2]
|
| 345 |
+
|
| 346 |
+
if not data.get("tone"):
|
| 347 |
+
if lang == "fr":
|
| 348 |
+
tone = "ludique" if variant == "best" else "sincère"
|
| 349 |
+
else:
|
| 350 |
+
tone = "playful" if variant == "best" else "sincere"
|
| 351 |
+
else:
|
| 352 |
+
tone = str(data["tone"])
|
| 353 |
+
|
| 354 |
+
return {
|
| 355 |
+
"category": category_key,
|
| 356 |
+
"language": lang,
|
| 357 |
+
"questions": q,
|
| 358 |
+
"micro_actions": m,
|
| 359 |
+
"tone": tone,
|
| 360 |
+
"safety_notes": "",
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
|
| 365 |
prompt = build_prompt(lang, category_key, variant)
|
| 366 |
|
| 367 |
raw_text = None
|
|
|
|
| 372 |
|
| 373 |
parsed = try_parse_json(raw_text) if raw_text else None
|
| 374 |
|
| 375 |
+
if parsed:
|
| 376 |
+
normalized = normalize_output(parsed, lang, category_key, variant)
|
| 377 |
+
else:
|
| 378 |
+
# Fallback: sample from local examples so it never breaks
|
| 379 |
few = FEWSHOTS[lang][category_key]
|
| 380 |
+
questions_pool = few["questions"][:]
|
| 381 |
+
micro_pool = few["micro_actions"][:]
|
| 382 |
+
random.shuffle(questions_pool)
|
| 383 |
+
random.shuffle(micro_pool)
|
| 384 |
+
normalized = {
|
| 385 |
"category": category_key,
|
| 386 |
"language": lang,
|
| 387 |
+
"questions": (questions_pool + [""] * 4)[:4],
|
| 388 |
+
"micro_actions": (micro_pool + [""] * 2)[:2],
|
| 389 |
+
"tone": "fallback",
|
| 390 |
+
"safety_notes": "",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
}
|
| 392 |
|
|
|
|
|
|
|
| 393 |
return {
|
| 394 |
"questions": normalized["questions"],
|
| 395 |
"micro_actions": normalized["micro_actions"],
|
|
|
|
| 397 |
}
|
| 398 |
|
| 399 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 400 |
+
# UI – pastel, animated, card-based
|
| 401 |
|
| 402 |
|
| 403 |
CUSTOM_CSS = """
|
|
|
|
| 408 |
--nv-accent: #f38a6b;
|
| 409 |
--nv-accent-soft: #ffe4d4;
|
| 410 |
--nv-accent-teal: #9fcfd1;
|
|
|
|
| 411 |
--nv-text-main: #262626;
|
| 412 |
--nv-text-muted: #6c6459;
|
| 413 |
}
|
|
|
|
| 421 |
position: relative;
|
| 422 |
}
|
| 423 |
|
| 424 |
+
/* animated blobs */
|
| 425 |
@keyframes nvBlobFloat {
|
| 426 |
0% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.6; }
|
| 427 |
50% { transform: translate3d(10px, -8px, 0) scale(1.04); opacity: 0.9; }
|
|
|
|
| 592 |
}
|
| 593 |
"""
|
| 594 |
|
| 595 |
+
|
| 596 |
def _map_category(choice: str) -> str:
|
| 597 |
mapping = {
|
| 598 |
"alimentation 🍎": "alimentation",
|
|
|
|
| 604 |
return mapping.get(choice, "alimentation")
|
| 605 |
|
| 606 |
|
| 607 |
+
def update_cards(lang: str, category_choice: str, variant: str):
|
| 608 |
+
cat_key = _map_category(category_choice)
|
| 609 |
+
out = ai_generate(lang, cat_key, variant)
|
| 610 |
qs = out["questions"]
|
| 611 |
ms = out["micro_actions"]
|
| 612 |
+
|
| 613 |
+
def card_html(kind: str, title: str, body: str) -> str:
|
| 614 |
+
kind_attr = "question" if kind == "q" else "micro"
|
| 615 |
+
return f"<div class='nv-card' data-kind='{kind_attr}'><div class='nv-card-title'>{title}</div><div>{body}</div></div>"
|
| 616 |
+
|
| 617 |
+
q_cards = [card_html("q", f"Question {i+1}", qs[i] if i < len(qs) else "") for i in range(4)]
|
| 618 |
+
m_cards = [card_html("m", f"Micro-action {i+1}", ms[i] if i < len(ms) else "") for i in range(2)]
|
| 619 |
+
|
| 620 |
return (*q_cards, *m_cards, out["raw_json"])
|
| 621 |
|
| 622 |
+
# ────────────────────────────────────────────────────────────────────────────────
|
| 623 |
+
# GRADIO APP
|
| 624 |
+
|
| 625 |
|
| 626 |
with gr.Blocks(title="Neurovie – Question Studio") as demo:
|
| 627 |
gr.HTML(f"<style>{CUSTOM_CSS}</style>")
|
|
|
|
| 675 |
|
| 676 |
btn = gr.Button("Generate card set ✨")
|
| 677 |
|
| 678 |
+
# Question & micro-action cards
|
| 679 |
with gr.Row(elem_classes="nv-section"):
|
| 680 |
with gr.Column():
|
| 681 |
gr.HTML("<div class='nv-label'>Questions</div>")
|
| 682 |
+
with gr.Column(elem_classes="nv-card-grid"):
|
| 683 |
+
q1 = gr.HTML()
|
| 684 |
+
q2 = gr.HTML()
|
| 685 |
+
q3 = gr.HTML()
|
| 686 |
+
q4 = gr.HTML()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
with gr.Column():
|
| 688 |
gr.HTML("<div class='nv-label'>Micro-actions</div>")
|
| 689 |
+
with gr.Column(elem_classes="nv-card-grid"):
|
| 690 |
+
m1 = gr.HTML()
|
| 691 |
+
m2 = gr.HTML()
|
| 692 |
|
| 693 |
# JSON output (for dev)
|
| 694 |
with gr.Column(elem_classes=["nv-section", "nv-json"]):
|
|
|
|
| 699 |
show_label=False,
|
| 700 |
)
|
| 701 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
btn.click(
|
| 703 |
update_cards,
|
| 704 |
[lang, category, variant],
|