Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
CHANGED
|
@@ -25,8 +25,6 @@ MODEL_ID = os.environ.get("MODEL_ID", "google/gemma-2-2b-it")
|
|
| 25 |
# Personal access token from your Hugging Face account (Space secret).
|
| 26 |
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
REPO_PATH = "/data/questions.json" # where we store generated questions
|
| 31 |
|
| 32 |
CATEGORIES = [
|
|
@@ -54,6 +52,26 @@ GUIDES = {
|
|
| 54 |
},
|
| 55 |
}
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
# Few-shot + fallback pools
|
| 58 |
FEWSHOTS = {
|
| 59 |
"fr": {
|
|
@@ -258,7 +276,7 @@ def save_repo(data: Dict[str, List[str]]) -> None:
|
|
| 258 |
# PROMPT + MODEL HELPERS
|
| 259 |
|
| 260 |
|
| 261 |
-
def build_prompt(lang: str, category_key: str,
|
| 262 |
cat = next((c for c in CATEGORIES if c["key"] == category_key), None)
|
| 263 |
if not cat:
|
| 264 |
category_key = "alimentation"
|
|
@@ -266,13 +284,7 @@ def build_prompt(lang: str, category_key: str, variant: str) -> str:
|
|
| 266 |
|
| 267 |
guide = GUIDES[lang][category_key]
|
| 268 |
few = FEWSHOTS[lang][category_key]
|
| 269 |
-
|
| 270 |
-
if variant == "best":
|
| 271 |
-
tone_fr = "ludique, original, léger"
|
| 272 |
-
tone_en = "playful, original, light"
|
| 273 |
-
else:
|
| 274 |
-
tone_fr = "sincère, introspectif, doux"
|
| 275 |
-
tone_en = "sincere, introspective, gentle"
|
| 276 |
|
| 277 |
schema = (
|
| 278 |
"{\n"
|
|
@@ -280,7 +292,7 @@ def build_prompt(lang: str, category_key: str, variant: str) -> str:
|
|
| 280 |
' "language": "<fr|en>",\n'
|
| 281 |
' "questions": ["q1", "q2", "q3", "q4"],\n'
|
| 282 |
' "micro_actions": ["m1", "m2"],\n'
|
| 283 |
-
' "
|
| 284 |
' "safety_notes": ""\n'
|
| 285 |
"}"
|
| 286 |
)
|
|
@@ -295,7 +307,7 @@ Tu crées des cartes-question pour parler de routines du quotidien.
|
|
| 295 |
|
| 296 |
- Catégorie: {cat['fr']} {cat['icon']}.
|
| 297 |
- Focus: {guide}
|
| 298 |
-
-
|
| 299 |
- Format: 4 questions + 2 micro-actions, 1 phrase courte chacune.
|
| 300 |
- Style: concret, bienveillant, sans jugement.
|
| 301 |
- Interdit: conseils médicaux, diagnostics, emojis.
|
|
@@ -316,7 +328,7 @@ You create question-cards about everyday routines.
|
|
| 316 |
|
| 317 |
- Category: {cat['en']} {cat['icon']}.
|
| 318 |
- Focus: {guide}
|
| 319 |
-
-
|
| 320 |
- Format: 4 questions + 2 micro-actions, one short sentence each.
|
| 321 |
- Style: concrete, kind, non-judgmental.
|
| 322 |
- Forbidden: medical advice, diagnoses, emojis.
|
|
@@ -367,7 +379,6 @@ def try_parse_json(text: str) -> Optional[Dict[str, Any]]:
|
|
| 367 |
return None
|
| 368 |
|
| 369 |
|
| 370 |
-
# 🔧 SIMPLIFIED, ROBUST MODEL CALL (no secrets required)
|
| 371 |
def model_call(prompt: str) -> str:
|
| 372 |
"""
|
| 373 |
Call Hugging Face Inference API using the conversational (chat) task.
|
|
@@ -437,7 +448,7 @@ def normalize_output(
|
|
| 437 |
data: Dict[str, Any],
|
| 438 |
lang: str,
|
| 439 |
category_key: str,
|
| 440 |
-
|
| 441 |
) -> Dict[str, Any]:
|
| 442 |
"""
|
| 443 |
Make model output always valid, even if the model returns emojis, wrong category labels,
|
|
@@ -480,14 +491,10 @@ def normalize_output(
|
|
| 480 |
m = [str(x).strip() for x in data.get("micro_actions", []) if str(x).strip()]
|
| 481 |
m = (m + [""] * 2)[:2]
|
| 482 |
|
| 483 |
-
# ---
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
else:
|
| 488 |
-
tone = "playful" if variant == "best" else "sincere"
|
| 489 |
-
else:
|
| 490 |
-
tone = str(data.get("tone")).strip().lower()
|
| 491 |
|
| 492 |
# --- SAFETY NOTES ---
|
| 493 |
safety_notes = str(data.get("safety_notes", ""))
|
|
@@ -497,24 +504,24 @@ def normalize_output(
|
|
| 497 |
"language": lang,
|
| 498 |
"questions": q,
|
| 499 |
"micro_actions": m,
|
| 500 |
-
"
|
| 501 |
"safety_notes": safety_notes,
|
| 502 |
}
|
| 503 |
|
| 504 |
|
| 505 |
-
def ai_generate(lang: str, category_key: str,
|
| 506 |
"""
|
| 507 |
Try to call the model. If anything fails or JSON is invalid,
|
| 508 |
fall back to shuffling the few-shots and include a safety_notes message.
|
| 509 |
"""
|
| 510 |
-
prompt = build_prompt(lang, category_key,
|
| 511 |
|
| 512 |
try:
|
| 513 |
raw_text = model_call(prompt)
|
| 514 |
parsed = try_parse_json(raw_text) if raw_text else None
|
| 515 |
|
| 516 |
if parsed:
|
| 517 |
-
return normalize_output(parsed, lang, category_key,
|
| 518 |
|
| 519 |
# Model replied but not valid JSON
|
| 520 |
few = FEWSHOTS[lang][category_key]
|
|
@@ -527,7 +534,7 @@ def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
|
|
| 527 |
"language": lang,
|
| 528 |
"questions": (q_pool + [""] * 4)[:4],
|
| 529 |
"micro_actions": (m_pool + [""] * 2)[:2],
|
| 530 |
-
"
|
| 531 |
"safety_notes": (
|
| 532 |
"Model replied but JSON parsing failed. "
|
| 533 |
f"raw_text starts with: {repr(raw_text[:160])}"
|
|
@@ -546,7 +553,7 @@ def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
|
|
| 546 |
"language": lang,
|
| 547 |
"questions": (q_pool + [""] * 4)[:4],
|
| 548 |
"micro_actions": (m_pool + [""] * 2)[:2],
|
| 549 |
-
"
|
| 550 |
"safety_notes": f"Model call error: {type(e).__name__}: {e}",
|
| 551 |
}
|
| 552 |
|
|
@@ -557,7 +564,7 @@ def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
|
|
| 557 |
def get_questions_and_micro(
|
| 558 |
lang: str,
|
| 559 |
category_key: str,
|
| 560 |
-
|
| 561 |
seen: List[str],
|
| 562 |
) -> Dict[str, Any]:
|
| 563 |
"""
|
|
@@ -573,7 +580,7 @@ def get_questions_and_micro(
|
|
| 573 |
unseen_repo = [q for q in repo_qs if q and q not in seen_set]
|
| 574 |
|
| 575 |
used_ai = False
|
| 576 |
-
|
| 577 |
safety_notes = ""
|
| 578 |
|
| 579 |
if len(unseen_repo) >= 4:
|
|
@@ -584,14 +591,13 @@ def get_questions_and_micro(
|
|
| 584 |
m_pool = FEWSHOTS[lang][category_key]["micro_actions"][:]
|
| 585 |
random.shuffle(m_pool)
|
| 586 |
micro = (m_pool + ["", ""])[:2]
|
| 587 |
-
tone = "repo"
|
| 588 |
safety_notes = ""
|
| 589 |
else:
|
| 590 |
# need fresh AI content
|
| 591 |
-
ai_out = ai_generate(lang, category_key,
|
| 592 |
questions = ai_out["questions"]
|
| 593 |
micro = ai_out["micro_actions"]
|
| 594 |
-
|
| 595 |
safety_notes = ai_out.get("safety_notes", "")
|
| 596 |
used_ai = True
|
| 597 |
|
|
@@ -613,12 +619,11 @@ def get_questions_and_micro(
|
|
| 613 |
"questions": questions,
|
| 614 |
"micro_actions": micro,
|
| 615 |
"source": "ai" if used_ai else "repo",
|
| 616 |
-
"
|
| 617 |
"safety_notes": safety_notes,
|
| 618 |
}
|
| 619 |
|
| 620 |
-
#
|
| 621 |
-
# for the UI we only return questions + micro + updated seen.
|
| 622 |
return {
|
| 623 |
"questions": questions,
|
| 624 |
"micro_actions": micro,
|
|
@@ -633,10 +638,15 @@ def get_questions_and_micro(
|
|
| 633 |
def _map_category(choice: str) -> str:
|
| 634 |
mapping = {
|
| 635 |
"alimentation 🍎": "alimentation",
|
|
|
|
| 636 |
"mouvement 🦘": "mouvement",
|
|
|
|
| 637 |
"cerveau 🧠": "cerveau",
|
|
|
|
| 638 |
"liens 🤝": "liens",
|
|
|
|
| 639 |
"bien-etre 💬": "bien-etre",
|
|
|
|
| 640 |
}
|
| 641 |
return mapping.get(choice, "alimentation")
|
| 642 |
|
|
@@ -652,9 +662,9 @@ def _card_html(category_key: str, kind: str, title: str, body: str, delay_s: flo
|
|
| 652 |
)
|
| 653 |
|
| 654 |
|
| 655 |
-
def update_cards(lang: str, category_choice: str,
|
| 656 |
category_key = _map_category(category_choice)
|
| 657 |
-
result = get_questions_and_micro(lang, category_key,
|
| 658 |
questions = result["questions"]
|
| 659 |
micro = result["micro_actions"]
|
| 660 |
new_seen = result["seen"]
|
|
@@ -663,6 +673,13 @@ def update_cards(lang: str, category_choice: str, variant: str, seen: List[str])
|
|
| 663 |
delays_q = [0.05, 0.10, 0.15, 0.20]
|
| 664 |
delays_m = [0.25, 0.30]
|
| 665 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
q_htmls = []
|
| 667 |
for i in range(4):
|
| 668 |
text = questions[i] if i < len(questions) else ""
|
|
@@ -670,7 +687,7 @@ def update_cards(lang: str, category_choice: str, variant: str, seen: List[str])
|
|
| 670 |
_card_html(
|
| 671 |
category_key,
|
| 672 |
"q",
|
| 673 |
-
f"
|
| 674 |
text,
|
| 675 |
delays_q[i],
|
| 676 |
)
|
|
@@ -683,7 +700,7 @@ def update_cards(lang: str, category_choice: str, variant: str, seen: List[str])
|
|
| 683 |
_card_html(
|
| 684 |
category_key,
|
| 685 |
"m",
|
| 686 |
-
f"
|
| 687 |
text,
|
| 688 |
delays_m[i],
|
| 689 |
)
|
|
@@ -692,6 +709,83 @@ def update_cards(lang: str, category_choice: str, variant: str, seen: List[str])
|
|
| 692 |
return (*q_htmls, *m_htmls, new_seen)
|
| 693 |
|
| 694 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 696 |
# GRADIO APP
|
| 697 |
|
|
@@ -703,23 +797,15 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
|
|
| 703 |
|
| 704 |
seen_state = gr.State([]) # per-session list of seen questions
|
| 705 |
|
|
|
|
|
|
|
| 706 |
with gr.Column(elem_classes="nv-shell"):
|
| 707 |
-
gr.HTML(
|
| 708 |
-
"""
|
| 709 |
-
<div>
|
| 710 |
-
<div class="nv-badge">NEUROVIE · FINGER</div>
|
| 711 |
-
<div class="nv-title">Question Studio</div>
|
| 712 |
-
<div class="nv-subtitle">
|
| 713 |
-
Minimal prompts for rich conversations — 4 questions and 2 micro-actions par tirage.
|
| 714 |
-
</div>
|
| 715 |
-
</div>
|
| 716 |
-
"""
|
| 717 |
-
)
|
| 718 |
|
| 719 |
# Settings
|
| 720 |
with gr.Row(elem_classes="nv-section"):
|
| 721 |
with gr.Column():
|
| 722 |
-
gr.HTML("<div class='nv-label'>
|
| 723 |
lang = gr.Radio(
|
| 724 |
choices=["fr", "en"],
|
| 725 |
value="fr",
|
|
@@ -727,54 +813,62 @@ with gr.Blocks(title="Neurovie – Question Studio") as demo:
|
|
| 727 |
elem_classes="nv-pills",
|
| 728 |
)
|
| 729 |
with gr.Column():
|
| 730 |
-
gr.HTML("<div class='nv-label'>
|
| 731 |
-
|
| 732 |
-
choices=
|
| 733 |
-
value="
|
| 734 |
show_label=False,
|
| 735 |
elem_classes="nv-pills",
|
| 736 |
)
|
| 737 |
|
| 738 |
with gr.Column(elem_classes="nv-section"):
|
| 739 |
-
gr.HTML("<div class='nv-label'>
|
| 740 |
category = gr.Radio(
|
| 741 |
-
choices=[
|
| 742 |
-
|
| 743 |
-
"mouvement 🦘",
|
| 744 |
-
"cerveau 🧠",
|
| 745 |
-
"liens 🤝",
|
| 746 |
-
"bien-etre 💬",
|
| 747 |
-
],
|
| 748 |
-
value="alimentation 🍎",
|
| 749 |
show_label=False,
|
| 750 |
elem_classes="nv-pills",
|
| 751 |
)
|
| 752 |
|
| 753 |
-
btn = gr.Button("
|
| 754 |
|
| 755 |
# Question & micro-action cards
|
| 756 |
with gr.Row(elem_classes="nv-section"):
|
| 757 |
with gr.Column():
|
| 758 |
-
gr.HTML("<div class='nv-label'>
|
| 759 |
with gr.Column(elem_classes="nv-card-grid"):
|
| 760 |
q1 = gr.HTML()
|
| 761 |
q2 = gr.HTML()
|
| 762 |
q3 = gr.HTML()
|
| 763 |
q4 = gr.HTML()
|
| 764 |
with gr.Column():
|
| 765 |
-
gr.HTML("<div class='nv-label'>
|
| 766 |
with gr.Column(elem_classes="nv-card-grid"):
|
| 767 |
m1 = gr.HTML()
|
| 768 |
m2 = gr.HTML()
|
| 769 |
|
| 770 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 771 |
|
| 772 |
btn.click(
|
| 773 |
update_cards,
|
| 774 |
-
[lang, category,
|
| 775 |
[q1, q2, q3, q4, m1, m2, seen_state],
|
| 776 |
show_progress=False, # hide Gradio built-in progress indicator
|
| 777 |
-
)
|
| 778 |
|
| 779 |
|
| 780 |
if __name__ == "__main__":
|
|
|
|
| 25 |
# Personal access token from your Hugging Face account (Space secret).
|
| 26 |
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 27 |
|
|
|
|
|
|
|
| 28 |
REPO_PATH = "/data/questions.json" # where we store generated questions
|
| 29 |
|
| 30 |
CATEGORIES = [
|
|
|
|
| 52 |
},
|
| 53 |
}
|
| 54 |
|
| 55 |
+
# Themes
|
| 56 |
+
THEME_KEYS = ["family", "friends", "romance", "silly", "education"]
|
| 57 |
+
|
| 58 |
+
THEME_DESCRIPTIONS = {
|
| 59 |
+
"fr": {
|
| 60 |
+
"family": "Thème famille : liens intergénérationnels, rituels familiaux, souvenirs partagés.",
|
| 61 |
+
"friends": "Thème amis : complicité, soutien, moments légers, retrouvailles.",
|
| 62 |
+
"romance": "Thème romance : connexion, douceur, attention à l’autre, moments à deux.",
|
| 63 |
+
"silly": "Thème décalé : questions ludiques, inattendues, créatives, pour faire rire.",
|
| 64 |
+
"education": "Thème éducation : curiosité, apprentissages, petites découvertes du quotidien.",
|
| 65 |
+
},
|
| 66 |
+
"en": {
|
| 67 |
+
"family": "Family theme: intergenerational bonds, family rituals, shared memories.",
|
| 68 |
+
"friends": "Friends theme: support, playfulness, shared moments, reconnection.",
|
| 69 |
+
"romance": "Romance theme: connection, tenderness, attention to each other, moments for two.",
|
| 70 |
+
"silly": "Silly theme: playful, unexpected, creative questions that invite laughter.",
|
| 71 |
+
"education": "Education theme: curiosity, learning, tiny everyday discoveries.",
|
| 72 |
+
},
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
# Few-shot + fallback pools
|
| 76 |
FEWSHOTS = {
|
| 77 |
"fr": {
|
|
|
|
| 276 |
# PROMPT + MODEL HELPERS
|
| 277 |
|
| 278 |
|
| 279 |
+
def build_prompt(lang: str, category_key: str, theme: str) -> str:
|
| 280 |
cat = next((c for c in CATEGORIES if c["key"] == category_key), None)
|
| 281 |
if not cat:
|
| 282 |
category_key = "alimentation"
|
|
|
|
| 284 |
|
| 285 |
guide = GUIDES[lang][category_key]
|
| 286 |
few = FEWSHOTS[lang][category_key]
|
| 287 |
+
theme_desc = THEME_DESCRIPTIONS[lang].get(theme, "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
schema = (
|
| 290 |
"{\n"
|
|
|
|
| 292 |
' "language": "<fr|en>",\n'
|
| 293 |
' "questions": ["q1", "q2", "q3", "q4"],\n'
|
| 294 |
' "micro_actions": ["m1", "m2"],\n'
|
| 295 |
+
' "theme": "<family|friends|romance|silly|education>",\n'
|
| 296 |
' "safety_notes": ""\n'
|
| 297 |
"}"
|
| 298 |
)
|
|
|
|
| 307 |
|
| 308 |
- Catégorie: {cat['fr']} {cat['icon']}.
|
| 309 |
- Focus: {guide}
|
| 310 |
+
- Thème actuel: {theme_desc}
|
| 311 |
- Format: 4 questions + 2 micro-actions, 1 phrase courte chacune.
|
| 312 |
- Style: concret, bienveillant, sans jugement.
|
| 313 |
- Interdit: conseils médicaux, diagnostics, emojis.
|
|
|
|
| 328 |
|
| 329 |
- Category: {cat['en']} {cat['icon']}.
|
| 330 |
- Focus: {guide}
|
| 331 |
+
- Current theme: {theme_desc}
|
| 332 |
- Format: 4 questions + 2 micro-actions, one short sentence each.
|
| 333 |
- Style: concrete, kind, non-judgmental.
|
| 334 |
- Forbidden: medical advice, diagnoses, emojis.
|
|
|
|
| 379 |
return None
|
| 380 |
|
| 381 |
|
|
|
|
| 382 |
def model_call(prompt: str) -> str:
|
| 383 |
"""
|
| 384 |
Call Hugging Face Inference API using the conversational (chat) task.
|
|
|
|
| 448 |
data: Dict[str, Any],
|
| 449 |
lang: str,
|
| 450 |
category_key: str,
|
| 451 |
+
theme: str,
|
| 452 |
) -> Dict[str, Any]:
|
| 453 |
"""
|
| 454 |
Make model output always valid, even if the model returns emojis, wrong category labels,
|
|
|
|
| 491 |
m = [str(x).strip() for x in data.get("micro_actions", []) if str(x).strip()]
|
| 492 |
m = (m + [""] * 2)[:2]
|
| 493 |
|
| 494 |
+
# --- THEME ---
|
| 495 |
+
model_theme = str(data.get("theme", "")).strip().lower()
|
| 496 |
+
if model_theme not in THEME_KEYS:
|
| 497 |
+
model_theme = theme # fall back to selected theme key
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
|
| 499 |
# --- SAFETY NOTES ---
|
| 500 |
safety_notes = str(data.get("safety_notes", ""))
|
|
|
|
| 504 |
"language": lang,
|
| 505 |
"questions": q,
|
| 506 |
"micro_actions": m,
|
| 507 |
+
"theme": model_theme,
|
| 508 |
"safety_notes": safety_notes,
|
| 509 |
}
|
| 510 |
|
| 511 |
|
| 512 |
+
def ai_generate(lang: str, category_key: str, theme: str) -> Dict[str, Any]:
|
| 513 |
"""
|
| 514 |
Try to call the model. If anything fails or JSON is invalid,
|
| 515 |
fall back to shuffling the few-shots and include a safety_notes message.
|
| 516 |
"""
|
| 517 |
+
prompt = build_prompt(lang, category_key, theme)
|
| 518 |
|
| 519 |
try:
|
| 520 |
raw_text = model_call(prompt)
|
| 521 |
parsed = try_parse_json(raw_text) if raw_text else None
|
| 522 |
|
| 523 |
if parsed:
|
| 524 |
+
return normalize_output(parsed, lang, category_key, theme)
|
| 525 |
|
| 526 |
# Model replied but not valid JSON
|
| 527 |
few = FEWSHOTS[lang][category_key]
|
|
|
|
| 534 |
"language": lang,
|
| 535 |
"questions": (q_pool + [""] * 4)[:4],
|
| 536 |
"micro_actions": (m_pool + [""] * 2)[:2],
|
| 537 |
+
"theme": theme,
|
| 538 |
"safety_notes": (
|
| 539 |
"Model replied but JSON parsing failed. "
|
| 540 |
f"raw_text starts with: {repr(raw_text[:160])}"
|
|
|
|
| 553 |
"language": lang,
|
| 554 |
"questions": (q_pool + [""] * 4)[:4],
|
| 555 |
"micro_actions": (m_pool + [""] * 2)[:2],
|
| 556 |
+
"theme": theme,
|
| 557 |
"safety_notes": f"Model call error: {type(e).__name__}: {e}",
|
| 558 |
}
|
| 559 |
|
|
|
|
| 564 |
def get_questions_and_micro(
|
| 565 |
lang: str,
|
| 566 |
category_key: str,
|
| 567 |
+
theme: str,
|
| 568 |
seen: List[str],
|
| 569 |
) -> Dict[str, Any]:
|
| 570 |
"""
|
|
|
|
| 580 |
unseen_repo = [q for q in repo_qs if q and q not in seen_set]
|
| 581 |
|
| 582 |
used_ai = False
|
| 583 |
+
theme_used = theme
|
| 584 |
safety_notes = ""
|
| 585 |
|
| 586 |
if len(unseen_repo) >= 4:
|
|
|
|
| 591 |
m_pool = FEWSHOTS[lang][category_key]["micro_actions"][:]
|
| 592 |
random.shuffle(m_pool)
|
| 593 |
micro = (m_pool + ["", ""])[:2]
|
|
|
|
| 594 |
safety_notes = ""
|
| 595 |
else:
|
| 596 |
# need fresh AI content
|
| 597 |
+
ai_out = ai_generate(lang, category_key, theme)
|
| 598 |
questions = ai_out["questions"]
|
| 599 |
micro = ai_out["micro_actions"]
|
| 600 |
+
theme_used = ai_out.get("theme", theme)
|
| 601 |
safety_notes = ai_out.get("safety_notes", "")
|
| 602 |
used_ai = True
|
| 603 |
|
|
|
|
| 619 |
"questions": questions,
|
| 620 |
"micro_actions": micro,
|
| 621 |
"source": "ai" if used_ai else "repo",
|
| 622 |
+
"theme": theme_used,
|
| 623 |
"safety_notes": safety_notes,
|
| 624 |
}
|
| 625 |
|
| 626 |
+
# For UI we only return questions + micro + updated seen.
|
|
|
|
| 627 |
return {
|
| 628 |
"questions": questions,
|
| 629 |
"micro_actions": micro,
|
|
|
|
| 638 |
def _map_category(choice: str) -> str:
|
| 639 |
mapping = {
|
| 640 |
"alimentation 🍎": "alimentation",
|
| 641 |
+
"nutrition 🍎": "alimentation",
|
| 642 |
"mouvement 🦘": "mouvement",
|
| 643 |
+
"movement 🦘": "mouvement",
|
| 644 |
"cerveau 🧠": "cerveau",
|
| 645 |
+
"brain 🧠": "cerveau",
|
| 646 |
"liens 🤝": "liens",
|
| 647 |
+
"connections 🤝": "liens",
|
| 648 |
"bien-etre 💬": "bien-etre",
|
| 649 |
+
"well-being 💬": "bien-etre",
|
| 650 |
}
|
| 651 |
return mapping.get(choice, "alimentation")
|
| 652 |
|
|
|
|
| 662 |
)
|
| 663 |
|
| 664 |
|
| 665 |
+
def update_cards(lang: str, category_choice: str, theme: str, seen: List[str]):
|
| 666 |
category_key = _map_category(category_choice)
|
| 667 |
+
result = get_questions_and_micro(lang, category_key, theme, seen or [])
|
| 668 |
questions = result["questions"]
|
| 669 |
micro = result["micro_actions"]
|
| 670 |
new_seen = result["seen"]
|
|
|
|
| 673 |
delays_q = [0.05, 0.10, 0.15, 0.20]
|
| 674 |
delays_m = [0.25, 0.30]
|
| 675 |
|
| 676 |
+
if lang == "fr":
|
| 677 |
+
q_prefix = "Question"
|
| 678 |
+
m_prefix = "Micro-action"
|
| 679 |
+
else:
|
| 680 |
+
q_prefix = "Question"
|
| 681 |
+
m_prefix = "Micro-action"
|
| 682 |
+
|
| 683 |
q_htmls = []
|
| 684 |
for i in range(4):
|
| 685 |
text = questions[i] if i < len(questions) else ""
|
|
|
|
| 687 |
_card_html(
|
| 688 |
category_key,
|
| 689 |
"q",
|
| 690 |
+
f"{q_prefix} {i+1}",
|
| 691 |
text,
|
| 692 |
delays_q[i],
|
| 693 |
)
|
|
|
|
| 700 |
_card_html(
|
| 701 |
category_key,
|
| 702 |
"m",
|
| 703 |
+
f"{m_prefix} {i+1}",
|
| 704 |
text,
|
| 705 |
delays_m[i],
|
| 706 |
)
|
|
|
|
| 709 |
return (*q_htmls, *m_htmls, new_seen)
|
| 710 |
|
| 711 |
|
| 712 |
+
# ────────────────────────────────────────────────────────────────────────────────
|
| 713 |
+
# UI TEXT TRANSLATIONS
|
| 714 |
+
|
| 715 |
+
|
| 716 |
+
def get_ui_texts(lang: str) -> Dict[str, Any]:
|
| 717 |
+
if lang == "fr":
|
| 718 |
+
header = """
|
| 719 |
+
<div>
|
| 720 |
+
<div class="nv-badge">NEUROVIE · FINGER</div>
|
| 721 |
+
<div class="nv-title">Question Studio</div>
|
| 722 |
+
<div class="nv-subtitle">
|
| 723 |
+
Questions minimalistes pour conversations riches — 4 questions et 2 micro-actions par tirage.
|
| 724 |
+
</div>
|
| 725 |
+
</div>
|
| 726 |
+
"""
|
| 727 |
+
category_choices = [
|
| 728 |
+
"alimentation 🍎",
|
| 729 |
+
"mouvement 🦘",
|
| 730 |
+
"cerveau 🧠",
|
| 731 |
+
"liens 🤝",
|
| 732 |
+
"bien-etre 💬",
|
| 733 |
+
]
|
| 734 |
+
return {
|
| 735 |
+
"header_html": header,
|
| 736 |
+
"language_label": "Langue",
|
| 737 |
+
"theme_label": "Thème",
|
| 738 |
+
"category_label": "Catégorie",
|
| 739 |
+
"questions_label": "Questions",
|
| 740 |
+
"micro_label": "Micro-actions",
|
| 741 |
+
"button_text": "Générer un tirage ✨",
|
| 742 |
+
"category_choices": category_choices,
|
| 743 |
+
"category_default": "alimentation 🍎",
|
| 744 |
+
}
|
| 745 |
+
else:
|
| 746 |
+
header = """
|
| 747 |
+
<div>
|
| 748 |
+
<div class="nv-badge">NEUROVIE · FINGER</div>
|
| 749 |
+
<div class="nv-title">Question Studio</div>
|
| 750 |
+
<div class="nv-subtitle">
|
| 751 |
+
Minimal prompts for rich conversations — 4 questions and 2 micro-actions per draw.
|
| 752 |
+
</div>
|
| 753 |
+
</div>
|
| 754 |
+
"""
|
| 755 |
+
category_choices = [
|
| 756 |
+
"Nutrition 🍎",
|
| 757 |
+
"Movement 🦘",
|
| 758 |
+
"Brain 🧠",
|
| 759 |
+
"Connections 🤝",
|
| 760 |
+
"Well-being 💬",
|
| 761 |
+
]
|
| 762 |
+
return {
|
| 763 |
+
"header_html": header,
|
| 764 |
+
"language_label": "Language",
|
| 765 |
+
"theme_label": "Theme",
|
| 766 |
+
"category_label": "Category",
|
| 767 |
+
"questions_label": "Questions",
|
| 768 |
+
"micro_label": "Micro-actions",
|
| 769 |
+
"button_text": "Generate card set ✨",
|
| 770 |
+
"category_choices": category_choices,
|
| 771 |
+
"category_default": "Nutrition 🍎",
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
|
| 775 |
+
def update_ui_language(lang: str):
|
| 776 |
+
t = get_ui_texts(lang)
|
| 777 |
+
return (
|
| 778 |
+
t["header_html"],
|
| 779 |
+
f"<div class='nv-label'>{t['language_label']}</div>",
|
| 780 |
+
f"<div class='nv-label'>{t['theme_label']}</div>",
|
| 781 |
+
f"<div class='nv-label'>{t['category_label']}</div>",
|
| 782 |
+
f"<div class='nv-label'>{t['questions_label']}</div>",
|
| 783 |
+
f"<div class='nv-label'>{t['micro_label']}</div>",
|
| 784 |
+
gr.Radio.update(choices=t["category_choices"], value=t["category_default"]),
|
| 785 |
+
gr.Button.update(value=t["button_text"]),
|
| 786 |
+
)
|
| 787 |
+
|
| 788 |
+
|
| 789 |
# ────────────────────────────────────────────────────────────────────────────────
|
| 790 |
# GRADIO APP
|
| 791 |
|
|
|
|
| 797 |
|
| 798 |
seen_state = gr.State([]) # per-session list of seen questions
|
| 799 |
|
| 800 |
+
ui_texts = get_ui_texts("fr")
|
| 801 |
+
|
| 802 |
with gr.Column(elem_classes="nv-shell"):
|
| 803 |
+
header = gr.HTML(ui_texts["header_html"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
|
| 805 |
# Settings
|
| 806 |
with gr.Row(elem_classes="nv-section"):
|
| 807 |
with gr.Column():
|
| 808 |
+
lang_label_html = gr.HTML(f"<div class='nv-label'>{ui_texts['language_label']}</div>")
|
| 809 |
lang = gr.Radio(
|
| 810 |
choices=["fr", "en"],
|
| 811 |
value="fr",
|
|
|
|
| 813 |
elem_classes="nv-pills",
|
| 814 |
)
|
| 815 |
with gr.Column():
|
| 816 |
+
theme_label_html = gr.HTML(f"<div class='nv-label'>{ui_texts['theme_label']}</div>")
|
| 817 |
+
theme = gr.Radio(
|
| 818 |
+
choices=THEME_KEYS,
|
| 819 |
+
value="family",
|
| 820 |
show_label=False,
|
| 821 |
elem_classes="nv-pills",
|
| 822 |
)
|
| 823 |
|
| 824 |
with gr.Column(elem_classes="nv-section"):
|
| 825 |
+
category_label_html = gr.HTML(f"<div class='nv-label'>{ui_texts['category_label']}</div>")
|
| 826 |
category = gr.Radio(
|
| 827 |
+
choices=ui_texts["category_choices"],
|
| 828 |
+
value=ui_texts["category_default"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 829 |
show_label=False,
|
| 830 |
elem_classes="nv-pills",
|
| 831 |
)
|
| 832 |
|
| 833 |
+
btn = gr.Button(ui_texts["button_text"])
|
| 834 |
|
| 835 |
# Question & micro-action cards
|
| 836 |
with gr.Row(elem_classes="nv-section"):
|
| 837 |
with gr.Column():
|
| 838 |
+
questions_label_html = gr.HTML(f"<div class='nv-label'>{ui_texts['questions_label']}</div>")
|
| 839 |
with gr.Column(elem_classes="nv-card-grid"):
|
| 840 |
q1 = gr.HTML()
|
| 841 |
q2 = gr.HTML()
|
| 842 |
q3 = gr.HTML()
|
| 843 |
q4 = gr.HTML()
|
| 844 |
with gr.Column():
|
| 845 |
+
micro_label_html = gr.HTML(f"<div class='nv-label'>{ui_texts['micro_label']}</div>")
|
| 846 |
with gr.Column(elem_classes="nv-card-grid"):
|
| 847 |
m1 = gr.HTML()
|
| 848 |
m2 = gr.HTML()
|
| 849 |
|
| 850 |
+
# Update labels & category choices when language changes
|
| 851 |
+
lang.change(
|
| 852 |
+
fn=update_ui_language,
|
| 853 |
+
inputs=[lang],
|
| 854 |
+
outputs=[
|
| 855 |
+
header,
|
| 856 |
+
lang_label_html,
|
| 857 |
+
theme_label_html,
|
| 858 |
+
category_label_html,
|
| 859 |
+
questions_label_html,
|
| 860 |
+
micro_label_html,
|
| 861 |
+
category,
|
| 862 |
+
btn,
|
| 863 |
+
],
|
| 864 |
+
)
|
| 865 |
|
| 866 |
btn.click(
|
| 867 |
update_cards,
|
| 868 |
+
[lang, category, theme, seen_state],
|
| 869 |
[q1, q2, q3, q4, m1, m2, seen_state],
|
| 870 |
show_progress=False, # hide Gradio built-in progress indicator
|
| 871 |
+
)
|
| 872 |
|
| 873 |
|
| 874 |
if __name__ == "__main__":
|