kappai commited on
Commit
96e3feb
·
verified ·
1 Parent(s): 85ec83e

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +409 -0
app.py ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import gradio as gr
5
+ from typing import Dict, Any, List, Optional
6
+ from huggingface_hub import InferenceClient
7
+
8
+ # ────────────────────────────────────────────────────────────────────────────────
9
+ # CONFIG
10
+ MODEL_ID = os.environ.get("MODEL_ID", "meta-llama/Meta-Llama-3.1-8B-Instruct")
11
+ HF_TOKEN = os.environ.get("HF_TOKEN") # set in Space settings → Repository secrets
12
+
13
+ # ────────────────────────────────────────────────────────────────────────────────
14
+ CATEGORIES = [
15
+ {"key": "alimentation", "icon": "🍎", "fr": "Alimentation", "en": "Nutrition"},
16
+ {"key": "mouvement", "icon": "🦘", "fr": "Mouvement", "en": "Movement"},
17
+ {"key": "cerveau", "icon": "🧠", "fr": "Cerveau", "en": "Brain"},
18
+ {"key": "liens", "icon": "🤝", "fr": "Liens", "en": "Connections"},
19
+ {"key": "bien-etre", "icon": "💬", "fr": "Bien-être", "en": "Well-being"},
20
+ ]
21
+
22
+ # Category-specific guidance (stronger prompt conditioning)
23
+ GUIDES = {
24
+ "fr": {
25
+ "alimentation": "Focalise sur des habitudes simples: hydratation, fruits/légumes, rythme des repas, portions, collations intelligentes. Pas de régime strict, pas de moralisation.",
26
+ "mouvement": "Mise sur le NEAT (mouvements du quotidien): marche, escaliers, étirements courts, pauses actives, mini-rituels autour du café ou des appels.",
27
+ "cerveau": "Stimulation douce: curiosité, mémoire, respiration, mini-jeux, apprentissage de 2-3 minutes. Zéro jargon médical.",
28
+ "liens": "Interactions humaines simples: gratitude, messages courts, appel bref, se joindre à quelqu’un, sourire, entraide. Inclusif, bienveillant.",
29
+ "bien-etre": "Auto-soin micro: pauses, respiration, sommeil régulier, journal rapide, limiter les écrans un moment. Ton chaleureux, zéro culpabilisation."
30
+ },
31
+ "en": {
32
+ "alimentation": "Focus on simple habits: hydration, fruit/veg, meal rhythm, portions, smart snacks. No strict diets, no moralizing.",
33
+ "mouvement": "Lean into NEAT: daily walking, stairs, quick stretches, active breaks, tiny rituals around coffee or calls.",
34
+ "cerveau": "Gentle stimulation: curiosity, memory, breathing, tiny games, 2–3 min learning. No medical jargon.",
35
+ "liens": "Simple human interactions: gratitude, short messages, brief call, joining someone, smiling, helping. Inclusive, kind tone.",
36
+ "bien-etre": "Micro self-care: breaks, breathing, steady sleep, quick journaling, small screen limits. Warm tone, no guilt."
37
+ }
38
+ }
39
+
40
+ # Few-shot examples to anchor style (one per category per language)
41
+ FEWSHOTS = {
42
+ "fr": {
43
+ "alimentation": {
44
+ "questions": [
45
+ "Quelle boisson te donne envie de boire plus d’eau dans la journée ?",
46
+ "Quel ajout simple rend ton petit-déj plus rassasiant ?",
47
+ "Quand as-tu naturellement faim d’un fruit ou d’un yaourt ?"
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": {
55
+ "questions": [
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
+ ],
60
+ "micro_actions": [
61
+ "Monter un étage par les escaliers aujourd’hui.",
62
+ "Faire 5 étirements doux après le café."
63
+ ]
64
+ },
65
+ "cerveau": {
66
+ "questions": [
67
+ "Qu’est-ce qui a suscité ta curiosité aujourd’hui ?",
68
+ "Quel moment t’irait pour 3 minutes de respiration ?",
69
+ "Quel mini-jeu aimes-tu pour réveiller l’esprit (ex: 3 mots fléchés) ?"
70
+ ],
71
+ "micro_actions": [
72
+ "Programmer un minuteur de 3 minutes pour respirer.",
73
+ "Lire un paragraphe d’un sujet nouveau ce soir."
74
+ ]
75
+ },
76
+ "liens": {
77
+ "questions": [
78
+ "Qui pourrais-tu remercier aujourd’hui et comment ?",
79
+ "À qui enverrais-tu un message court pour reprendre contact ?",
80
+ "Avec qui partagerais-tu une courte marche cette semaine ?"
81
+ ],
82
+ "micro_actions": [
83
+ "Envoyer un message de gratitude à une personne.",
84
+ "Proposer une pause-café de 10 minutes."
85
+ ]
86
+ },
87
+ "bien-etre": {
88
+ "questions": [
89
+ "Quel signal t’indique qu’il est temps de faire une pause ?",
90
+ "Quelle routine de 2 minutes t’aide à te recentrer ?",
91
+ "Quel moment favorise un coucher plus régulier ?"
92
+ ],
93
+ "micro_actions": [
94
+ "Éteindre les écrans 10 minutes plus tôt ce soir.",
95
+ "Écrire 3 lignes sur ton humeur du jour."
96
+ ]
97
+ }
98
+ },
99
+ "en": {
100
+ "alimentation": {
101
+ "questions": [
102
+ "What drink makes you want to sip more water through the day?",
103
+ "What small add-on makes your breakfast more filling?",
104
+ "When do you naturally crave a fruit or yogurt?"
105
+ ],
106
+ "micro_actions": [
107
+ "Fill a water bottle this morning.",
108
+ "Add one fruit to your afternoon snack."
109
+ ]
110
+ },
111
+ "mouvement": {
112
+ "questions": [
113
+ "Which short trip could you walk at least once this week?",
114
+ "Which 2-minute active break fits between two tasks?",
115
+ "What makes you move without noticing (e.g., walking on calls)?"
116
+ ],
117
+ "micro_actions": [
118
+ "Take one flight of stairs today.",
119
+ "Do 5 light stretches after coffee."
120
+ ]
121
+ },
122
+ "cerveau": {
123
+ "questions": [
124
+ "What sparked your curiosity today?",
125
+ "When could you do 3 minutes of breathing?",
126
+ "Which mini-game wakes you up (e.g., 3 crossword clues)?"
127
+ ],
128
+ "micro_actions": [
129
+ "Set a 3-minute timer to breathe.",
130
+ "Read one paragraph on a new topic tonight."
131
+ ]
132
+ },
133
+ "liens": {
134
+ "questions": [
135
+ "Who could you thank today—and how?",
136
+ "Who might you text briefly to reconnect?",
137
+ "Who could you invite for a short walk this week?"
138
+ ],
139
+ "micro_actions": [
140
+ "Send a gratitude message to one person.",
141
+ "Offer a 10-minute coffee break."
142
+ ]
143
+ },
144
+ "bien-etre": {
145
+ "questions": [
146
+ "What cue tells you it’s time for a pause?",
147
+ "What 2-minute routine helps you reset?",
148
+ "What time supports a steadier bedtime?"
149
+ ],
150
+ "micro_actions": [
151
+ "Turn screens off 10 minutes earlier tonight.",
152
+ "Write three lines about your mood today."
153
+ ]
154
+ }
155
+ }
156
+ }
157
+
158
+ def build_prompt(lang: str, category_key: str, variant: str) -> str:
159
+ cat = next((c for c in CATEGORIES if c["key"] == category_key), None)
160
+ if not cat:
161
+ category_key = "alimentation"
162
+ cat = next((c for c in CATEGORIES if c["key"] == category_key), None)
163
+
164
+ # Variant tone
165
+ variant_fr = (
166
+ "Contrainte de ton: *ludique et original* (‘meilleur’)."
167
+ if variant == "best" else
168
+ "Contrainte de ton: *introspectif et authentique* (‘plus sincère’)."
169
+ )
170
+ variant_en = (
171
+ "Tone constraint: *playful and original* (‘best’)."
172
+ if variant == "best" else
173
+ "Tone constraint: *introspective and authentic* (‘most sincere’)."
174
+ )
175
+
176
+ # Shared schema
177
+ schema = (
178
+ "{\n"
179
+ ' "category": "<category_key>",\n'
180
+ ' "language": "<fr|en>",\n'
181
+ ' "questions": ["q1", "q2", "q3"],\n'
182
+ ' "micro_actions": ["m1", "m2"],\n'
183
+ ' "tone": "playful|sincere|ludique|sincère",\n'
184
+ ' "safety_notes": "short coaching tips"\n'
185
+ "}"
186
+ )
187
+
188
+ # Safety & style guardrails
189
+ safety_fr = (
190
+ "Règles de sécurité et de style:\n"
191
+ "- Zéro conseil médical, pas de diagnostic, pas d’injonction.\n"
192
+ "- Langage bienveillant, inclusif, non culpabilisant.\n"
193
+ "- Items concrets, réalisables en 1–3 minutes quand c’est pertinent.\n"
194
+ "- Phrases courtes. Pas de listes imbriquées. Pas d’emojis dans la sortie.\n"
195
+ "- Réponds STRICTEMENT en JSON valide, sans texte autour."
196
+ )
197
+ safety_en = (
198
+ "Safety & style rules:\n"
199
+ "- No medical advice, no diagnosis, no injunctions.\n"
200
+ "- Kind, inclusive, non-judgmental language.\n"
201
+ "- Concrete items, doable in 1–3 minutes when relevant.\n"
202
+ "- Short sentences. No nested lists. No emojis in output.\n"
203
+ "- Respond in STRICT, valid JSON only, with nothing else."
204
+ )
205
+
206
+ guide = GUIDES[lang][category_key]
207
+ few = FEWSHOTS[lang][category_key]
208
+
209
+ if lang == "fr":
210
+ system = (
211
+ "Tu es l’IA du jeu de cartes Neurovie, inspiré du modèle FINGER. "
212
+ "Une carte = une question sur les routines (alimentation, mouvement, cerveau, liens, bien-être)."
213
+ )
214
+ rules = (
215
+ "Mode d’emploi (style 7 familles):\n"
216
+ "- Demande une carte (catégorie FINGER) avec une question précise.\n"
217
+ "- Si l’autre joueur l’a, il la donne et continue. Sinon, tu pioches.\n"
218
+ "Variante: chacun pioche à son tour; le meilleur ou le plus sincère garde la carte."
219
+ )
220
+ user = (
221
+ f"Catégorie: {cat['fr']} {cat['icon']}. {variant_fr}\n"
222
+ f"Guide de ciblage: {guide}\n"
223
+ "Produit attendu: 3 QUESTIONS et 2 MICRO-ACTIONS, chacune en 1 phrase, concrète.\n"
224
+ "Adapte le ton selon la contrainte. Respecte scrupuleusement le schéma JSON.\n"
225
+ "Exemple de style (à imiter, ne pas répéter tel quel):\n"
226
+ f"- Questions: {few['questions']}\n"
227
+ f"- Micro-actions: {few['micro_actions']}\n"
228
+ "Schéma JSON strict:\n"
229
+ f"{schema}\n"
230
+ "RENVOIE UNIQUEMENT DU JSON VALIDE."
231
+ )
232
+ content = f"{system}\n\n{rules}\n\n{safety_fr}\n\n{user}"
233
+ else:
234
+ system = (
235
+ "You are the AI for the Neurovie card game, inspired by the FINGER model. "
236
+ "One card = one question about daily routines (nutrition, movement, brain, connections, well-being)."
237
+ )
238
+ rules = (
239
+ "How to play (7-families style):\n"
240
+ "- Ask for a specific card (FINGER category) using a clear question.\n"
241
+ "- If they have it, they give it and keep asking; otherwise, draw.\n"
242
+ "Variant: players draw in turn; the best or most sincere keeps the card."
243
+ )
244
+ user = (
245
+ f"Category: {cat['en']} {cat['icon']}. {variant_en}\n"
246
+ f"Targeting guide: {guide}\n"
247
+ "Expected output: 3 QUESTIONS and 2 MICRO-ACTIONS, each one sentence, concrete.\n"
248
+ "Match the requested tone. Follow the JSON schema exactly.\n"
249
+ "Style example (imitate tone; do not copy verbatim):\n"
250
+ f"- Questions: {few['questions']}\n"
251
+ f"- Micro-actions: {few['micro_actions']}\n"
252
+ "Strict JSON schema:\n"
253
+ f"{schema}\n"
254
+ "RETURN JSON ONLY."
255
+ )
256
+ content = f"{system}\n\n{rules}\n\n{safety_en}\n\n{user}"
257
+
258
+ return content
259
+
260
+ # ────────────────────────────────────────────────────────────────────────────────
261
+ # Inference helpers
262
+
263
+ def try_parse_json(text: str) -> Optional[Dict[str, Any]]:
264
+ if not text:
265
+ return None
266
+ # Grab last JSON object
267
+ match = re.search(r"\{[\s\S]*\}\s*$", text.strip())
268
+ if not match:
269
+ return None
270
+ try:
271
+ return json.loads(match.group(0))
272
+ except Exception:
273
+ return None
274
+
275
+ def normalize_output(data: Dict[str, Any], lang: str, category_key: str, variant: str) -> Dict[str, Any]:
276
+ """Ensure exactly 3 questions, 2 micro_actions, set tone, fill missing fields."""
277
+ q = data.get("questions", [])
278
+ m = data.get("micro_actions", [])
279
+ # basic sanitization
280
+ q = [str(s).strip().rstrip() for s in q if str(s).strip()]
281
+ m = [str(s).strip().rstrip() for s in m if str(s).strip()]
282
+ # clip/extend
283
+ q = (q + [""] * 3)[:3]
284
+ m = (m + [""] * 2)[:2]
285
+
286
+ tone = data.get("tone", "")
287
+ if not tone:
288
+ tone = ("ludique" if (lang == "fr" and variant == "best")
289
+ else "sincère" if lang == "fr"
290
+ else "playful" if variant == "best"
291
+ else "sincere")
292
+
293
+ safety = data.get("safety_notes", "")
294
+ if not safety:
295
+ safety = "Reste bienveillant·e, évite les conseils médicaux." if lang == "fr" else "Be kind, avoid medical advice."
296
+
297
+ out = {
298
+ "category": category_key,
299
+ "language": lang,
300
+ "questions": q,
301
+ "micro_actions": m,
302
+ "tone": tone,
303
+ "safety_notes": safety,
304
+ }
305
+ return out
306
+
307
+ def model_call(prompt: str) -> str:
308
+ client = InferenceClient(model=MODEL_ID, token=HF_TOKEN)
309
+
310
+ # Prefer chat endpoint when available
311
+ try:
312
+ resp = client.chat.completions.create(
313
+ model=MODEL_ID,
314
+ messages=[{"role": "user", "content": prompt}],
315
+ temperature=0.85,
316
+ top_p=0.92,
317
+ max_tokens=220,
318
+ )
319
+ content = resp.choices[0].message["content"] if hasattr(resp.choices[0], "message") else resp.choices[0].message.content
320
+ if isinstance(content, list):
321
+ content = "".join(part.get("text", "") if isinstance(part, dict) else str(part) for part in content)
322
+ return content.strip()
323
+ except Exception:
324
+ pass
325
+
326
+ # Fallback to text_generation
327
+ resp = client.text_generation(
328
+ prompt,
329
+ max_new_tokens=220,
330
+ temperature=0.85,
331
+ top_p=0.92,
332
+ return_full_text=False,
333
+ )
334
+ return resp.strip()
335
+
336
+ def generate(lang: str, category_key: str, variant: str) -> Dict[str, str]:
337
+ prompt = build_prompt(lang, category_key, variant)
338
+
339
+ raw_text = None
340
+ try:
341
+ raw_text = model_call(prompt)
342
+ except Exception:
343
+ raw_text = None
344
+
345
+ parsed = try_parse_json(raw_text) if raw_text else None
346
+
347
+ # Local fallback if needed (pull from fewshots to keep style tight)
348
+ if not parsed:
349
+ few = FEWSHOTS[lang][category_key]
350
+ parsed = {
351
+ "category": category_key,
352
+ "language": lang,
353
+ "questions": few["questions"][:3],
354
+ "micro_actions": few["micro_actions"][:2],
355
+ "tone": ("ludique" if (lang == "fr" and variant == "best")
356
+ else "sincère" if lang == "fr"
357
+ else "playful" if variant == "best"
358
+ else "sincere"),
359
+ "safety_notes": "Reste bienveillant·e, évite les conseils médicaux." if lang == "fr" else "Be kind, avoid medical advice.",
360
+ }
361
+ raw_text = "(local few-shot fallback used)"
362
+
363
+ normalized = normalize_output(parsed, lang, category_key, variant)
364
+
365
+ pretty = [
366
+ f"Category: {normalized['category']} | Lang: {normalized['language']} | Tone: {normalized['tone']}",
367
+ "",
368
+ "Questions:",
369
+ *[f"• {q}" for q in normalized["questions"]],
370
+ "",
371
+ "Micro-actions:",
372
+ *[f"• {m}" for m in normalized["micro_actions"]],
373
+ ]
374
+ if normalized.get("safety_notes"):
375
+ pretty += ["", f"Notes: {normalized['safety_notes']}"]
376
+
377
+ return {
378
+ "pretty_text": "\n".join(pretty),
379
+ "raw_json": json.dumps(normalized, ensure_ascii=False, indent=2)
380
+ }
381
+
382
+ # ────────────────────────────────────────────────────────────────────────────────
383
+ # GRADIO UI
384
+ with gr.Blocks(title="Neurovie – FINGER Question Generator") as demo:
385
+ gr.Markdown("# Neurovie – FINGER Question Generator")
386
+ gr.Markdown("Aggressively guided prompts with category-aware examples. Strict JSON, then friendly display.")
387
+
388
+ with gr.Row():
389
+ lang = gr.Radio(choices=["fr", "en"], value="fr", label="Lang / Language")
390
+ category = gr.Radio(
391
+ choices=[c["key"] for c in CATEGORIES],
392
+ value="alimentation",
393
+ label="Catégorie / Category"
394
+ )
395
+ variant = gr.Radio(choices=["best", "sincere"], value="best", label="Variante")
396
+
397
+ btn = gr.Button("Generate")
398
+
399
+ pretty = gr.Textbox(label="Output", lines=14)
400
+ raw_json = gr.Code(label="Parsed JSON", language="json")
401
+
402
+ def on_click(l, c, v):
403
+ out = generate(l, c, v)
404
+ return out["pretty_text"], out["raw_json"]
405
+
406
+ btn.click(on_click, inputs=[lang, category, variant], outputs=[pretty, raw_json])
407
+
408
+ if __name__ == "__main__":
409
+ demo.launch()