kappai commited on
Commit
7cff386
·
verified ·
1 Parent(s): dc1df5c

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +168 -158
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 in Space settings Repository secrets
13
 
14
  CATEGORIES = [
15
  {"key": "alimentation", "icon": "🍎", "fr": "Alimentation", "en": "Nutrition"},
@@ -36,7 +38,7 @@ GUIDES = {
36
  },
37
  }
38
 
39
- # Few-shot examples: now 4 questions each
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 & MODEL
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
- 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'
@@ -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": "short coaching tips"\n'
200
  "}"
201
  )
202
 
 
 
 
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]]:
@@ -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.85,
301
  top_p=0.92,
302
- max_tokens=220,
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=220,
322
- temperature=0.85,
323
  top_p=0.92,
324
  return_full_text=False,
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
@@ -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 not parsed:
 
 
 
340
  few = FEWSHOTS[lang][category_key]
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"
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, cards
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 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>")
@@ -638,26 +675,20 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
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"]):
@@ -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],