kappai commited on
Commit
dc1df5c
·
verified ·
1 Parent(s): 24521f4

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +163 -120
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": "Focalise sur des habitudes simples: hydratation, fruits/légumes, rythme des repas, portions, collations intelligentes. Pas de régime strict, pas de moralisation.",
27
- "mouvement": "Mise sur le NEAT (mouvements du quotidien): marche, escaliers, étirements courts, pauses actives, mini-rituels autour du café ou des appels.",
28
- "cerveau": "Stimulation douce: curiosité, mémoire, respiration, mini-jeux, apprentissage de 2-3 minutes. Zéro jargon médical.",
29
- "liens": "Interactions humaines simples: gratitude, messages courts, appel bref, se joindre à quelqu’un, sourire, entraide. Inclusif, bienveillant.",
30
- "bien-etre": "Auto-soin micro: pauses, respiration, sommeil régulier, journal rapide, limiter les écrans un moment. Ton chaleureux, zéro culpabilisation.",
31
  },
32
  "en": {
33
- "alimentation": "Focus on simple habits: hydration, fruit/veg, meal rhythm, portions, smart snacks. No strict diets, no moralizing.",
34
- "mouvement": "Lean into NEAT: daily walking, stairs, quick stretches, active breaks, tiny rituals around coffee or calls.",
35
- "cerveau": "Gentle stimulation: curiosity, memory, breathing, tiny games, 2–3 min learning. No medical jargon.",
36
- "liens": "Simple human interactions: gratitude, short messages, brief call, joining someone, smiling, helping. Inclusive, kind tone.",
37
- "bien-etre": "Micro self-care: breaks, breathing, steady sleep, quick journaling, small screen limits. Warm tone, no guilt.",
38
  },
39
  }
40
 
41
- # Few-shot examples to anchor style (one per category per language)
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 HELPERS
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
- "Contrainte de ton: ludique et original (‘meilleur’)."
174
  if variant == "best"
175
- else "Contrainte de ton: introspectif et authentique (‘plus sincère’)."
176
  )
177
  variant_en = (
178
- "Tone constraint: playful and original (‘best’)."
179
  if variant == "best"
180
- else "Tone constraint: introspective and authentic (‘most sincere’)."
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 (alimentation, mouvement, cerveau, liens, bien-être)."
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 de sécurité et de style:\n"
207
- "- Zéro conseil médical, pas de diagnostic, pas d’injonction.\n"
208
- "- Langage bienveillant, inclusif, non culpabilisant.\n"
209
- "- Items concrets, réalisables en 1–3 minutes quand c’est pertinent.\n"
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"Guide de ciblage: {guide}\n"
215
- "Produit attendu: 3 QUESTIONS et 2 MICRO-ACTIONS, chacune en 1 phrase, concrète.\n"
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 (nutrition, movement, brain, connections, well-being)."
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
- "Safety & style rules:\n"
237
- "- No medical advice, no diagnosis, no orders.\n"
238
- "- Kind, inclusive, non-judgmental language.\n"
239
- "- Concrete items, doable in 1–3 minutes when relevant.\n"
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"Targeting guide: {guide}\n"
245
- "Expected output: 3 QUESTIONS and 2 MICRO-ACTIONS, each one sentence, concrete.\n"
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
- return f"{system}\n\n{rules}\n\n{safety}\n\n{user}"
 
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
- q = (q + [""] * 3)[:3]
 
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, 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"][:3],
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
- "pretty_text": "\n".join(lines),
 
390
  "raw_json": json.dumps(normalized, ensure_ascii=False, indent=2),
391
  }
392
 
393
  # ────────────────────────────────────────────────────────────────────────────────
394
- # UI – minimal, palette-matched, animated
 
395
 
396
  CUSTOM_CSS = """
397
  :root {
398
- --nv-bg: #f6f1ea;
399
  --nv-card: #fffdf8;
400
- --nv-border: #e3d7c7;
401
- --nv-accent: #f27c5e;
402
- --nv-accent-soft: #ffe0cf;
403
- --nv-accent-teal: #7ab7b7;
 
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, #fff5eb 0, #f6f1ea 50%, #f2ede5 100%);
412
- max-width: 900px !important;
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.65; }
420
  50% { transform: translate3d(10px, -8px, 0) scale(1.04); opacity: 0.9; }
421
- 100% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.65; }
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%, #ffd1bf, transparent 60%);
439
  }
440
  .gradio-container::after {
441
  bottom: -120px;
442
  right: -40px;
443
- background: radial-gradient(circle at 70% 70%, #bfe8ff, transparent 60%);
444
  animation-delay: 4s;
445
  }
446
 
447
- /* main card */
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, #ffe0cf, #ffd2c0) !important;
508
  border-color: var(--nv-accent) !important;
509
  color: var(--nv-text-main) !important;
510
- box-shadow: 0 4px 10px rgba(242, 124, 94, 0.2);
511
  transform: translateY(-1px);
512
  }
513
 
514
  /* button */
515
  @keyframes nvPulse {
516
- 0% { transform: translateY(0) scale(1); box-shadow: 0 10px 22px rgba(242, 124, 94, 0.30); }
517
- 50% { transform: translateY(-1px) scale(1.02); box-shadow: 0 14px 26px rgba(242, 124, 94, 0.40); }
518
- 100% { transform: translateY(0) scale(1); box-shadow: 0 10px 22px rgba(242, 124, 94, 0.30); }
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, #ffe0cf, transparent 55%),
528
- linear-gradient(135deg, #f6a37d, #f27c5e) !important;
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
- /* output fade */
537
- @keyframes nvFadeIn {
538
- 0% { opacity: 0; transform: translateY(4px); }
539
- 100% { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
540
  }
541
- .nv-output {
542
- background: #f6f8fc;
543
  border-radius: 18px;
544
- border: 1px solid #d8e2f2;
545
- padding: 14px;
546
- font-size: 0.93rem;
547
- animation: nvFadeIn 350ms ease-out;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  }
549
 
550
- /* code box */
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
- return out["pretty_text"], out["raw_json"]
 
 
 
 
 
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 3 questions and 2 micro-actions.
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
- with gr.Column(elem_classes="nv-section"):
637
- gr.HTML("<div class='nv-label'>Preview</div>")
638
- pretty = gr.Textbox(
639
- label="",
640
- lines=12,
641
- show_label=False,
642
- elem_classes="nv-output",
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
- btn.click(click_handler, [lang, category, variant], [pretty, raw_json])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()