kappai commited on
Commit
db8d943
Β·
verified Β·
1 Parent(s): cfd53f5

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +210 -59
app.py CHANGED
@@ -10,8 +10,11 @@ from huggingface_hub import InferenceClient
10
  # ────────────────────────────────────────────────────────────────────────────────
11
  # CONFIG
12
 
13
- MODEL_ID = os.environ.get("MODEL_ID", "meta-llama/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,7 +41,7 @@ GUIDES = {
38
  },
39
  }
40
 
41
- # Few-shot examples for style + fallback
42
  FEWSHOTS = {
43
  "fr": {
44
  "alimentation": {
@@ -206,6 +209,38 @@ FEWSHOTS = {
206
  },
207
  }
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  # ────────────────────────────────────────────────────────────────────────────────
210
  # PROMPT + MODEL HELPERS
211
 
@@ -248,7 +283,7 @@ Tu crΓ©es des cartes-question pour parler de routines du quotidien.
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
 
@@ -269,7 +304,7 @@ You create question-cards about everyday routines.
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
 
@@ -339,7 +374,6 @@ def normalize_output(
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
 
@@ -363,7 +397,6 @@ def normalize_output(
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
368
  try:
369
  raw_text = model_call(prompt)
@@ -374,30 +407,92 @@ def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
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"],
396
- "raw_json": json.dumps(normalized, ensure_ascii=False, indent=2),
 
 
 
397
  }
398
 
 
399
  # ────────────────────────────────────────────────────────────────────────────────
400
- # UI – pastel, animated, card-based
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
 
403
  CUSTOM_CSS = """
@@ -407,11 +502,11 @@ CUSTOM_CSS = """
407
  --nv-border: #e5d8c7;
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
  }
414
 
 
415
  .gradio-container {
416
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
417
  background: radial-gradient(circle at top left, #fff6ee 0, #f7f2ec 50%, #f2ece6 100%);
@@ -421,7 +516,7 @@ CUSTOM_CSS = """
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; }
@@ -450,7 +545,7 @@ CUSTOM_CSS = """
450
  animation-delay: 4s;
451
  }
452
 
453
- /* main shell */
454
  @keyframes nvCardIn {
455
  0% { opacity: 0; transform: translateY(8px) scale(0.98); }
456
  100% { opacity: 1; transform: translateY(0) scale(1); }
@@ -517,7 +612,7 @@ CUSTOM_CSS = """
517
  transform: translateY(-1px);
518
  }
519
 
520
- /* button */
521
  @keyframes nvPulse {
522
  0% { transform: translateY(0) scale(1); box-shadow: 0 10px 22px rgba(243, 138, 107, 0.30); }
523
  50% { transform: translateY(-1px) scale(1.02); box-shadow: 0 14px 26px rgba(243, 138, 107, 0.40); }
@@ -539,39 +634,56 @@ CUSTOM_CSS = """
539
  animation-duration: 1.4s;
540
  }
541
 
542
- /* cards */
543
- @keyframes nvCardFloat {
544
- 0% { transform: translateY(0); }
545
- 50% { transform: translateY(-2px); }
546
- 100% { transform: translateY(0); }
547
- }
548
  .nv-card-grid {
549
  display: grid;
550
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
551
  gap: 10px;
552
  }
 
 
 
 
 
 
553
  .nv-card {
554
- background: #ffffff;
555
  border-radius: 18px;
556
  padding: 10px 11px;
557
  font-size: 0.9rem;
558
  border: 1px solid rgba(0,0,0,0.03);
559
  box-shadow: 0 6px 14px rgba(15, 23, 42, 0.06);
560
- animation: nvCardFloat 6s ease-in-out infinite;
 
 
 
 
 
 
 
 
561
  }
562
- .nv-card[data-kind="question"] {
563
- background: radial-gradient(circle at 0 0, #f9ecff 0, #ffffff 60%);
564
- border-color: #e0d2f5;
565
  }
566
- .nv-card[data-kind="micro"] {
567
- background: radial-gradient(circle at 0 0, #e6f7f8 0, #ffffff 60%);
568
- border-color: #cfe6e6;
569
  }
 
 
 
 
 
 
 
 
 
570
  .nv-card-title {
571
  font-size: 0.72rem;
572
  letter-spacing: 0.14em;
573
  text-transform: uppercase;
574
- color: #9a8fb6;
575
  margin-bottom: 4px;
576
  }
577
 
@@ -604,20 +716,57 @@ def _map_category(choice: str) -> str:
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
@@ -626,6 +775,8 @@ def update_cards(lang: str, category_choice: str, variant: str):
626
  with gr.Blocks(title="Neurovie – Question Studio") as demo:
627
  gr.HTML(f"<style>{CUSTOM_CSS}</style>")
628
 
 
 
629
  with gr.Column(elem_classes="nv-shell"):
630
  gr.HTML(
631
  """
@@ -633,7 +784,7 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
633
  <div class="nv-badge">NEUROVIE Β· FINGER</div>
634
  <div class="nv-title">Question Studio</div>
635
  <div class="nv-subtitle">
636
- Minimal prompts for rich conversations β€” draw 4 questions and 2 micro-actions.
637
  </div>
638
  </div>
639
  """
@@ -701,8 +852,8 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
701
 
702
  btn.click(
703
  update_cards,
704
- [lang, category, variant],
705
- [q1, q2, q3, q4, m1, m2, raw_json],
706
  )
707
 
708
  if __name__ == "__main__":
 
10
  # ────────────────────────────────────────────────────────────────────────────────
11
  # CONFIG
12
 
13
+ # You can hard-code the model here, or set MODEL_ID as a Space secret.
14
+ MODEL_ID = os.environ.get("MODEL_ID")
15
+ HF_TOKEN = os.environ.get("HF_TOKEN") # optional for this public model
16
+
17
+ REPO_PATH = "/data/questions.json" # where we store generated questions
18
 
19
  CATEGORIES = [
20
  {"key": "alimentation", "icon": "🍎", "fr": "Alimentation", "en": "Nutrition"},
 
41
  },
42
  }
43
 
44
+ # Few-shot + fallback pools
45
  FEWSHOTS = {
46
  "fr": {
47
  "alimentation": {
 
209
  },
210
  }
211
 
212
+ # ────────────────────────────────────────────────────────────────────────────────
213
+ # REPOSITORY HELPERS (questions only, per category)
214
+
215
+
216
+ def _default_repo() -> Dict[str, List[str]]:
217
+ return {c["key"]: [] for c in CATEGORIES}
218
+
219
+
220
+ def load_repo() -> Dict[str, List[str]]:
221
+ os.makedirs(os.path.dirname(REPO_PATH), exist_ok=True)
222
+ if not os.path.exists(REPO_PATH):
223
+ data = _default_repo()
224
+ with open(REPO_PATH, "w", encoding="utf-8") as f:
225
+ json.dump(data, f, ensure_ascii=False, indent=2)
226
+ return data
227
+ try:
228
+ with open(REPO_PATH, "r", encoding="utf-8") as f:
229
+ data = json.load(f)
230
+ except Exception:
231
+ data = _default_repo()
232
+ # Ensure all categories exist
233
+ base = _default_repo()
234
+ base.update({k: v for k, v in data.items() if isinstance(v, list)})
235
+ return base
236
+
237
+
238
+ def save_repo(data: Dict[str, List[str]]) -> None:
239
+ os.makedirs(os.path.dirname(REPO_PATH), exist_ok=True)
240
+ with open(REPO_PATH, "w", encoding="utf-8") as f:
241
+ json.dump(data, f, ensure_ascii=False, indent=2)
242
+
243
+
244
  # ────────────────────────────────────────────────────────────────────────────────
245
  # PROMPT + MODEL HELPERS
246
 
 
283
  - CatΓ©gorie: {cat['fr']} {cat['icon']}.
284
  - Focus: {guide}
285
  - Ton: {tone_fr}
286
+ - Format: 4 questions + 2 micro-actions, 1 phrase courte chacune.
287
  - Style: concret, bienveillant, sans jugement.
288
  - Interdit: conseils mΓ©dicaux, diagnostics, emojis.
289
 
 
304
  - Category: {cat['en']} {cat['icon']}.
305
  - Focus: {guide}
306
  - Tone: {tone_en}
307
+ - Format: 4 questions + 2 micro-actions, one short sentence each.
308
  - Style: concrete, kind, non-judgmental.
309
  - Forbidden: medical advice, diagnoses, emojis.
310
 
 
374
  q = [str(x).strip() for x in data.get("questions", []) if str(x).strip()]
375
  m = [str(x).strip() for x in data.get("micro_actions", []) if str(x).strip()]
376
 
 
377
  q = (q + [""] * 4)[:4]
378
  m = (m + [""] * 2)[:2]
379
 
 
397
 
398
  def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
399
  prompt = build_prompt(lang, category_key, variant)
 
400
  raw_text = None
401
  try:
402
  raw_text = model_call(prompt)
 
407
 
408
  if parsed:
409
  normalized = normalize_output(parsed, lang, category_key, variant)
410
+ return normalized
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
 
412
+ # fallback: shuffle fewshots
413
+ few = FEWSHOTS[lang][category_key]
414
+ q_pool = few["questions"][:]
415
+ m_pool = few["micro_actions"][:]
416
+ random.shuffle(q_pool)
417
+ random.shuffle(m_pool)
418
  return {
419
+ "category": category_key,
420
+ "language": lang,
421
+ "questions": (q_pool + [""] * 4)[:4],
422
+ "micro_actions": (m_pool + [""] * 2)[:2],
423
+ "tone": "fallback",
424
+ "safety_notes": "",
425
  }
426
 
427
+
428
  # ────────────────────────────────────────────────────────────────────────────────
429
+ # MAIN LOGIC: REPO + SESSION "SEEN" QUESTIONS
430
+
431
+
432
+ def get_questions_and_micro(
433
+ lang: str,
434
+ category_key: str,
435
+ variant: str,
436
+ seen: List[str],
437
+ ) -> Dict[str, Any]:
438
+ """
439
+ 1. Load /data/questions.json
440
+ 2. If repo has >=4 unseen questions -> sample 4 from repo, micro from local pool.
441
+ 3. Else -> call AI, store any new questions into repo, use AI's questions + micro.
442
+ 4. Update seen list so this session won't see the same question twice.
443
+ """
444
+ seen_set = set(seen or [])
445
+ repo = load_repo()
446
+ repo_qs = repo.get(category_key, [])
447
+
448
+ unseen_repo = [q for q in repo_qs if q and q not in seen_set]
449
+
450
+ used_ai = False
451
+
452
+ if len(unseen_repo) >= 4:
453
+ # entirely from repo, no AI call
454
+ random.shuffle(unseen_repo)
455
+ questions = unseen_repo[:4]
456
+ # micro-actions from local fewshot pool (cheap)
457
+ m_pool = FEWSHOTS[lang][category_key]["micro_actions"][:]
458
+ random.shuffle(m_pool)
459
+ micro = (m_pool + ["", ""])[:2]
460
+ else:
461
+ # need fresh AI content
462
+ ai_out = ai_generate(lang, category_key, variant)
463
+ questions = ai_out["questions"]
464
+ micro = ai_out["micro_actions"]
465
+ used_ai = True
466
+
467
+ # store new questions in repo
468
+ new_qs = [q for q in questions if q and q not in repo_qs]
469
+ if new_qs:
470
+ repo_qs_extended = repo_qs + new_qs
471
+ repo[category_key] = repo_qs_extended
472
+ save_repo(repo)
473
+
474
+ # update seen for this session
475
+ for q in questions:
476
+ if q:
477
+ seen_set.add(q)
478
+
479
+ payload = {
480
+ "category": category_key,
481
+ "language": lang,
482
+ "questions": questions,
483
+ "micro_actions": micro,
484
+ "source": "ai" if used_ai else "repo",
485
+ }
486
+ return {
487
+ "questions": questions,
488
+ "micro_actions": micro,
489
+ "raw_json": json.dumps(payload, ensure_ascii=False, indent=2),
490
+ "seen": list(seen_set),
491
+ }
492
+
493
+
494
+ # ────────────────────────────────────────────────────────────────────────────────
495
+ # UI – pastel, animated, color-coded cards
496
 
497
 
498
  CUSTOM_CSS = """
 
502
  --nv-border: #e5d8c7;
503
  --nv-accent: #f38a6b;
504
  --nv-accent-soft: #ffe4d4;
 
505
  --nv-text-main: #262626;
506
  --nv-text-muted: #6c6459;
507
  }
508
 
509
+ /* page */
510
  .gradio-container {
511
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
512
  background: radial-gradient(circle at top left, #fff6ee 0, #f7f2ec 50%, #f2ece6 100%);
 
516
  position: relative;
517
  }
518
 
519
+ /* floating blobs */
520
  @keyframes nvBlobFloat {
521
  0% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.6; }
522
  50% { transform: translate3d(10px, -8px, 0) scale(1.04); opacity: 0.9; }
 
545
  animation-delay: 4s;
546
  }
547
 
548
+ /* main panel */
549
  @keyframes nvCardIn {
550
  0% { opacity: 0; transform: translateY(8px) scale(0.98); }
551
  100% { opacity: 1; transform: translateY(0) scale(1); }
 
612
  transform: translateY(-1px);
613
  }
614
 
615
+ /* button (breathing) */
616
  @keyframes nvPulse {
617
  0% { transform: translateY(0) scale(1); box-shadow: 0 10px 22px rgba(243, 138, 107, 0.30); }
618
  50% { transform: translateY(-1px) scale(1.02); box-shadow: 0 14px 26px rgba(243, 138, 107, 0.40); }
 
634
  animation-duration: 1.4s;
635
  }
636
 
637
+ /* cards grid */
 
 
 
 
 
638
  .nv-card-grid {
639
  display: grid;
640
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
641
  gap: 10px;
642
  }
643
+
644
+ /* deal animation */
645
+ @keyframes nvCardDeal {
646
+ 0% { opacity: 0; transform: translate3d(0, 0, 0) scale(0.7); }
647
+ 100% { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
648
+ }
649
  .nv-card {
 
650
  border-radius: 18px;
651
  padding: 10px 11px;
652
  font-size: 0.9rem;
653
  border: 1px solid rgba(0,0,0,0.03);
654
  box-shadow: 0 6px 14px rgba(15, 23, 42, 0.06);
655
+ opacity: 0;
656
+ transform: scale(0.7);
657
+ animation: nvCardDeal 420ms ease-out forwards;
658
+ }
659
+
660
+ /* color-coded by category */
661
+ .nv-card--cat-alimentation {
662
+ background: radial-gradient(circle at 0 0, #e3f7df 0, #ffffff 60%);
663
+ border-color: #c3e5bc;
664
  }
665
+ .nv-card--cat-mouvement {
666
+ background: radial-gradient(circle at 0 0, #dbeeff 0, #ffffff 60%);
667
+ border-color: #b9d8ff;
668
  }
669
+ .nv-card--cat-cerveau {
670
+ background: radial-gradient(circle at 0 0, #ffd6d6 0, #ffffff 60%);
671
+ border-color: #f7b6b6;
672
  }
673
+ .nv-card--cat-liens {
674
+ background: radial-gradient(circle at 0 0, #ffead1 0, #ffffff 60%);
675
+ border-color: #f6d2a3;
676
+ }
677
+ .nv-card--cat-bien-etre {
678
+ background: radial-gradient(circle at 0 0, #ffe1f2 0, #ffffff 60%);
679
+ border-color: #f3c3e4;
680
+ }
681
+
682
  .nv-card-title {
683
  font-size: 0.72rem;
684
  letter-spacing: 0.14em;
685
  text-transform: uppercase;
686
+ color: #7c6a9a;
687
  margin-bottom: 4px;
688
  }
689
 
 
716
  return mapping.get(choice, "alimentation")
717
 
718
 
719
+ def _card_html(category_key: str, kind: str, title: str, body: str, delay_s: float) -> str:
720
+ kind_attr = "question" if kind == "q" else "micro"
721
+ cat_class = f"nv-card--cat-{category_key}"
722
+ # each card has its own animation delay => "dealing" feel
723
+ return (
724
+ f"<div class='nv-card {cat_class}' data-kind='{kind_attr}' "
725
+ f"style='animation-delay:{delay_s:.2f}s'><div class='nv-card-title'>{title}</div>"
726
+ f"<div>{body}</div></div>"
727
+ )
728
 
 
 
 
729
 
730
+ def update_cards(lang: str, category_choice: str, variant: str, seen: List[str]):
731
+ category_key = _map_category(category_choice)
732
+ result = get_questions_and_micro(lang, category_key, variant, seen or [])
733
+ questions = result["questions"]
734
+ micro = result["micro_actions"]
735
+ raw_json = result["raw_json"]
736
+ new_seen = result["seen"]
737
+
738
+ # Stagger cards a bit
739
+ delays_q = [0.05, 0.10, 0.15, 0.20]
740
+ delays_m = [0.25, 0.30]
741
+
742
+ q_htmls = []
743
+ for i in range(4):
744
+ text = questions[i] if i < len(questions) else ""
745
+ q_htmls.append(
746
+ _card_html(
747
+ category_key,
748
+ "q",
749
+ f"Question {i+1}",
750
+ text,
751
+ delays_q[i],
752
+ )
753
+ )
754
+
755
+ m_htmls = []
756
+ for i in range(2):
757
+ text = micro[i] if i < len(micro) else ""
758
+ m_htmls.append(
759
+ _card_html(
760
+ category_key,
761
+ "m",
762
+ f"Micro-action {i+1}",
763
+ text,
764
+ delays_m[i],
765
+ )
766
+ )
767
+
768
+ return (*q_htmls, *m_htmls, raw_json, new_seen)
769
 
 
770
 
771
  # ────────────────────────────────────────────────────────────────────────────────
772
  # GRADIO APP
 
775
  with gr.Blocks(title="Neurovie – Question Studio") as demo:
776
  gr.HTML(f"<style>{CUSTOM_CSS}</style>")
777
 
778
+ seen_state = gr.State([]) # per-session list of seen questions
779
+
780
  with gr.Column(elem_classes="nv-shell"):
781
  gr.HTML(
782
  """
 
784
  <div class="nv-badge">NEUROVIE Β· FINGER</div>
785
  <div class="nv-title">Question Studio</div>
786
  <div class="nv-subtitle">
787
+ Minimal prompts for rich conversations β€” 4 questions and 2 micro-actions par tirage.
788
  </div>
789
  </div>
790
  """
 
852
 
853
  btn.click(
854
  update_cards,
855
+ [lang, category, variant, seen_state],
856
+ [q1, q2, q3, q4, m1, m2, raw_json, seen_state],
857
  )
858
 
859
  if __name__ == "__main__":