goumsss commited on
Commit
53940d1
Β·
1 Parent(s): fd0b9df

Rename to LoveMath + major UX improvements

Browse files

- Rename app MathAnimaux β†’ LoveMath
- Translate all UI text to minimal English
- Add emoji selector (animals + places) before quiz
- Background pre-generation of reward images via ThreadPoolExecutor
- Emoji→text mapping injected into FLUX prompt
- Comprehensive error handling throughout
- Update README with local run instructions

Files changed (4) hide show
  1. README.md +44 -18
  2. app.py +248 -156
  3. image_generator.py +75 -49
  4. math_engine.py +9 -9
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
- title: MathAnimaux
3
- emoji: 🐾
4
  colorFrom: purple
5
  colorTo: pink
6
  sdk: gradio
@@ -16,26 +16,52 @@ tags:
16
  - math
17
  ---
18
 
19
- # 🐾 MathAnimaux
20
 
21
- Une application de calcul mental pour les enfants, avec des rΓ©compenses sous forme d'images d'animaux gΓ©nΓ©rΓ©es par l'IA !
22
 
23
- ## Comment Γ§a marche ?
24
 
25
- 1. Entre ton prΓ©nom
26
- 2. RΓ©ponds aux questions de calcul mental
27
- 3. Toutes les **3 bonnes rΓ©ponses**, tu gagnes une image d'un animal trop mignon gΓ©nΓ©rΓ© par IA !
28
- 4. Monte de niveau : Additions β†’ Soustractions β†’ Multiplications β†’ MΓ©lange
29
 
30
- ## Niveaux
31
 
32
- | Niveau | OpΓ©ration | Objectif |
33
- |--------|-----------|----------|
34
- | 1 | βž• Additions | 5 bonnes rΓ©ponses |
35
- | 2 | βž– Soustractions | 5 bonnes rΓ©ponses |
36
- | 3 | βœ–οΈ Multiplications | 5 bonnes rΓ©ponses |
37
- | 4 | 🎲 Mélange | Mode expert infini |
38
 
39
- ## Modèle utilisé
40
 
41
- Images gΓ©nΓ©rΓ©es avec **FLUX.1-schnell** (12B paramΓ¨tres, Apache 2.0) via πŸ€— Diffusers.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: LoveMath
3
+ emoji: πŸ’–
4
  colorFrom: purple
5
  colorTo: pink
6
  sdk: gradio
 
16
  - math
17
  ---
18
 
19
+ # πŸ’– LoveMath
20
 
21
+ A mental math app for kids β€” answer questions, earn cute AI-generated animal images as rewards!
22
 
23
+ ## How it works
24
 
25
+ 1. Enter your name
26
+ 2. Pick your favourite animals 🐾 and places 🌍 (used to personalise reward images)
27
+ 3. Answer math questions β€” every **3 correct answers** earns a reward image
28
+ 4. Level up: **Additions β†’ Subtractions β†’ Multiplications β†’ Mix**
29
 
30
+ ## Levels
31
 
32
+ | Level | Operation | Goal |
33
+ |-------|-----------|------|
34
+ | 1 | βž• Additions | 5 correct |
35
+ | 2 | βž– Subtractions | 5 correct |
36
+ | 3 | βœ–οΈ Multiplications | 5 correct |
37
+ | 4 | 🎲 Mix | Endless |
38
 
39
+ ## Image model
40
 
41
+ Rewards generated with **FLUX.1-schnell** (12B params, Apache 2.0) via πŸ€— Diffusers.
42
+ Images start generating in the background as soon as the quiz begins.
43
+
44
+ ## Run locally
45
+
46
+ > Requires Python on Apple Silicon (arm64). Recommended: [Miniforge](https://github.com/conda-forge/miniforge).
47
+
48
+ ```bash
49
+ # 1. Install dependencies
50
+ ~/miniforge3/bin/pip install -r requirements.txt
51
+
52
+ # 2. Accept FLUX.1-schnell license on HuggingFace
53
+ # β†’ https://huggingface.co/black-forest-labs/FLUX.1-schnell
54
+ # Then log in:
55
+ hf auth login
56
+
57
+ # 3. Run
58
+ ~/miniforge3/bin/python3 app.py
59
+ # Opens at http://localhost:7860
60
+ # First run downloads ~23GB of model weights (cached after that)
61
+ ```
62
+
63
+ > On standard Python/pip (non-Apple Silicon):
64
+ > ```bash
65
+ > pip install -r requirements.txt
66
+ > python app.py
67
+ > ```
app.py CHANGED
@@ -1,242 +1,334 @@
1
  """
2
- 🐾 MathAnimaux β€” Application de calcul mental avec rΓ©compenses mignonnes !
3
- Pour Léa et Zoé 🌟
4
  """
5
 
6
- import gradio as gr
 
7
  from math_engine import generate_question, LEVEL_NAMES, LEVEL_THRESHOLDS, level_up_message
8
  from image_generator import generate_reward_image
9
 
10
  # ---------------------------------------------------------------------------
11
- # How many correct answers trigger a reward image
12
  # ---------------------------------------------------------------------------
13
- REWARD_EVERY = 3
14
 
 
 
 
 
 
 
15
 
16
  # ---------------------------------------------------------------------------
17
- # Core game logic
18
  # ---------------------------------------------------------------------------
19
 
20
- def start_game(player_name: str, state: dict):
21
- """Initialize or reset the game for a player."""
22
- name = player_name.strip() or "Champion(ne)"
23
- state = {
24
- "name": name,
25
- "level": 1,
26
- "score": 0,
27
- "streak": 0,
28
- "correct_since_reward": 0,
29
- "level_correct": 0,
30
- "question": "",
31
- "answer": 0,
32
- "waiting_next": False,
33
- }
34
- question, answer = generate_question(state["level"])
35
- state["question"] = question
36
- state["answer"] = answer
37
-
38
- status = _status_text(state)
 
39
  return (
40
  state,
41
- gr.update(visible=False), # hide welcome panel
42
- gr.update(visible=True), # show game panel
43
- f"Bonjour **{name}** ! C'est parti πŸš€",
44
- status,
45
- f"## {question} = ?",
46
- "", # clear input
47
- "", # clear feedback
48
- None, # clear image
49
- gr.update(visible=False), # hide reward panel
50
  )
51
 
 
 
 
52
 
53
- def check_answer(user_input: str, state: dict):
54
- """Validate the answer and update game state."""
55
- if not state or state.get("waiting_next"):
56
- return (state,) + _no_change_outputs(state)
57
-
58
  try:
59
- user_answer = int(user_input.strip())
60
- except ValueError:
 
61
  return (
62
  state,
63
- _status_text(state),
64
- f"## {state['question']} = ?",
65
- "",
66
- "⚠️ Γ‰cris un nombre entier !",
67
- None,
68
- gr.update(visible=False),
69
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
- correct = user_answer == state["answer"]
72
-
73
- if correct:
74
- state["score"] += 1
75
- state["streak"] += 1
76
- state["correct_since_reward"] += 1
77
- state["level_correct"] += 1
78
-
79
- # Check level-up
80
- level_msg = ""
81
- leveled_up = False
82
- threshold = LEVEL_THRESHOLDS.get(state["level"], 999)
83
- if state["level_correct"] >= threshold and state["level"] < 4:
84
- state["level"] += 1
85
- state["level_correct"] = 0
86
- level_msg = level_up_message(state["level"])
87
- leveled_up = True
88
-
89
- # Check reward
90
  reward_image = None
91
  reward_visible = False
92
- if state["correct_since_reward"] >= REWARD_EVERY:
93
- state["correct_since_reward"] = 0
94
- reward_image, _ = generate_reward_image(state["streak"])
95
- reward_visible = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  # Next question
98
  question, answer = generate_question(state["level"])
99
  state["question"] = question
100
- state["answer"] = answer
101
-
102
- streak_emoji = "πŸ”₯" * min(state["streak"], 5)
103
- if leveled_up:
104
- feedback = f"βœ… Correct ! {level_msg}"
105
- else:
106
- feedback = f"βœ… Bravo ! {streak_emoji}" if state["streak"] > 1 else "βœ… Correct !"
107
 
108
  return (
109
  state,
110
  _status_text(state),
111
  f"## {question} = ?",
112
- "",
113
  feedback,
114
  reward_image,
115
  gr.update(visible=reward_visible),
116
  )
117
 
118
- else:
119
- state["streak"] = 0
120
- feedback = f"❌ Raté ! La réponse était **{state['answer']}**. Essaie encore !"
121
- # New question after wrong answer
122
- question, answer = generate_question(state["level"])
123
  state["question"] = question
124
- state["answer"] = answer
125
  return (
126
  state,
127
  _status_text(state),
128
  f"## {question} = ?",
129
  "",
130
- feedback,
131
  None,
132
  gr.update(visible=False),
133
  )
134
 
135
 
136
- def _status_text(state: dict) -> str:
137
- if not state:
138
- return ""
139
- level_name = LEVEL_NAMES.get(state["level"], "")
140
- return (
141
- f"πŸ‘€ {state['name']} | "
142
- f"⭐ Score : {state['score']} | "
143
- f"πŸ“š Niveau : {level_name}"
144
- )
145
-
146
-
147
- def _no_change_outputs(state):
148
- return (
149
- _status_text(state),
150
- f"## {state.get('question', '')} = ?",
151
- "",
152
- "",
153
- None,
154
- gr.update(visible=False),
155
- )
156
-
157
 
158
  # ---------------------------------------------------------------------------
159
  # Gradio UI
160
  # ---------------------------------------------------------------------------
161
 
 
 
162
  CSS = """
163
- #title { text-align: center; font-size: 2em; margin-bottom: 0.2em; }
164
- #subtitle { text-align: center; color: #888; margin-bottom: 1em; }
165
- #question-box { text-align: center; font-size: 2.5em; font-weight: bold; padding: 0.5em; }
166
- #feedback-box { text-align: center; font-size: 1.3em; min-height: 2em; }
167
- #status-box { text-align: center; padding: 0.4em; background: #f9f0ff; border-radius: 8px; }
168
- #reward-img { border-radius: 16px; }
169
  .answer-input input { font-size: 2em !important; text-align: center !important; }
 
170
  """
171
 
172
- with gr.Blocks(title="🐾 MathAnimaux", theme=gr.themes.Soft(primary_hue="purple", secondary_hue="pink")) as demo:
173
 
174
  state = gr.State({})
175
 
176
- # ---- Header ----
177
- gr.Markdown("# 🐾 MathAnimaux", elem_id="title")
178
- gr.Markdown("*Calcule bien et gagne des images d'animaux trop mignons !*", elem_id="subtitle")
179
 
180
- # ---- Welcome panel ----
181
  with gr.Group(visible=True) as welcome_panel:
182
- gr.Markdown("### πŸ‘‹ Qui es-tu ?")
183
- player_name = gr.Textbox(
184
- placeholder="Γ‰cris ton prΓ©nom…",
185
- label="Ton prΓ©nom",
186
- max_lines=1,
 
 
 
 
 
 
 
 
 
 
 
 
187
  )
188
- start_btn = gr.Button("πŸš€ Commencer !", variant="primary", size="lg")
 
189
 
190
- # ---- Game panel ----
191
  with gr.Group(visible=False) as game_panel:
192
- hello_msg = gr.Markdown("")
193
- status_md = gr.Markdown("", elem_id="status-box", container=True)
194
-
195
  question_md = gr.Markdown("", elem_id="question-box")
196
 
197
  answer_input = gr.Textbox(
198
- placeholder="Ta rΓ©ponse…",
199
- label="Ta rΓ©ponse",
200
  max_lines=1,
201
  elem_classes=["answer-input"],
202
  )
203
- submit_btn = gr.Button("βœ”οΈ Valider", variant="primary")
204
-
205
  feedback_md = gr.Markdown("", elem_id="feedback-box")
206
 
207
- # ---- Reward panel ----
208
  with gr.Group(visible=False) as reward_panel:
209
- gr.Markdown("### 🎁 Bravo ! Voici ta récompense !")
210
  reward_image = gr.Image(
211
- label="",
212
- show_label=False,
213
- elem_id="reward-img",
214
- height=400,
215
  )
216
 
217
- # ---- Restart button (always visible once game started) ----
218
- restart_btn = gr.Button("πŸ”„ Recommencer", variant="secondary", size="sm")
219
-
220
- # ---- Wiring ----
221
- start_outputs = [
222
- state, welcome_panel, game_panel,
223
- hello_msg, status_md, question_md,
224
- answer_input, feedback_md, reward_image, reward_panel,
225
- ]
226
- start_btn.click(start_game, inputs=[player_name, state], outputs=start_outputs)
227
- player_name.submit(start_game, inputs=[player_name, state], outputs=start_outputs)
228
-
229
- check_outputs = [
230
- state, status_md, question_md, answer_input, feedback_md, reward_image, reward_panel,
231
- ]
232
- submit_btn.click(check_answer, inputs=[answer_input, state], outputs=check_outputs)
233
  answer_input.submit(check_answer, inputs=[answer_input, state], outputs=check_outputs)
234
 
 
235
  restart_btn.click(
236
- lambda: (gr.update(visible=True), gr.update(visible=False), {}, "", ""),
237
- outputs=[welcome_panel, game_panel, state, player_name, hello_msg],
 
238
  )
239
 
240
 
241
  if __name__ == "__main__":
242
- demo.launch(css=CSS)
 
 
 
 
1
  """
2
+ πŸ’– LoveMath β€” Math practice with cute AI-generated animal rewards!
 
3
  """
4
 
5
+ import random
6
+ import concurrent.futures
7
  from math_engine import generate_question, LEVEL_NAMES, LEVEL_THRESHOLDS, level_up_message
8
  from image_generator import generate_reward_image
9
 
10
  # ---------------------------------------------------------------------------
11
+ # Constants
12
  # ---------------------------------------------------------------------------
 
13
 
14
+ REWARD_EVERY = 3 # correct answers needed to trigger a reward
15
+
16
+ ANIMAL_EMOJIS = ["🐢", "🐱", "🐰", "🦊", "🐼", "🐨", "🦁", "🐯", "🐸", "🐧", "πŸ¦‹", "πŸ¦„"]
17
+ PLACE_EMOJIS = ["🌊", "πŸ”οΈ", "🌸", "🌈", "πŸŒ™", "⭐", "🌴", "🏑", "🌺", "πŸ„"]
18
+
19
+ _executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
20
 
21
  # ---------------------------------------------------------------------------
22
+ # State helpers
23
  # ---------------------------------------------------------------------------
24
 
25
+ def _empty_state() -> dict:
26
+ return {}
27
+
28
+ def _status_text(state: dict) -> str:
29
+ if not state:
30
+ return ""
31
+ level_name = LEVEL_NAMES.get(state["level"], "")
32
+ return f"πŸ‘€ {state['name']} | ⭐ {state['score']} | πŸ“š {level_name}"
33
+
34
+ def _submit_generation(state: dict):
35
+ """Fire off a background image generation job and store Future in state."""
36
+ animals = state.get("selected_animals", [random.choice(ANIMAL_EMOJIS)])
37
+ places = state.get("selected_places", [random.choice(PLACE_EMOJIS)])
38
+ streak = state.get("streak", 0)
39
+ future = _executor.submit(generate_reward_image, streak, animals, places)
40
+ state["future"] = future
41
+ state["awaiting_reward"] = False
42
+
43
+ def _no_change_outputs(state: dict):
44
+ import gradio as gr
45
  return (
46
  state,
47
+ _status_text(state),
48
+ f"## {state.get('question', '')} = ?",
49
+ "",
50
+ "",
51
+ None,
52
+ gr.update(visible=False),
 
 
 
53
  )
54
 
55
+ # ---------------------------------------------------------------------------
56
+ # Game flow
57
+ # ---------------------------------------------------------------------------
58
 
59
+ def enter_name(player_name: str, state: dict):
60
+ """Step 1: validate name, move to emoji picker."""
 
 
 
61
  try:
62
+ import gradio as gr
63
+ name = player_name.strip() or "Player"
64
+ state = {"name": name}
65
  return (
66
  state,
67
+ gr.update(visible=False), # hide welcome
68
+ gr.update(visible=True), # show emoji picker
69
+ gr.update(visible=False), # hide game
70
+ random.sample(ANIMAL_EMOJIS, 1), # pre-select 1 animal
71
+ random.sample(PLACE_EMOJIS, 1), # pre-select 1 place
 
72
  )
73
+ except Exception as e:
74
+ print(f"enter_name error: {e}")
75
+ import gradio as gr
76
+ return state, gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), [], []
77
+
78
+
79
+ def start_game(animal_sel: list, place_sel: list, state: dict):
80
+ """Step 2: emoji confirmed β†’ start quiz + fire background image generation."""
81
+ import gradio as gr
82
+ try:
83
+ # Validate selections (1–3 each required)
84
+ if not animal_sel or not place_sel:
85
+ return (state, gr.update(visible=True), gr.update(visible=False), "⚠️ Pick at least one animal and one place!", "", "")
86
+
87
+ state["selected_animals"] = animal_sel[:3]
88
+ state["selected_places"] = place_sel[:3]
89
+ state["level"] = 1
90
+ state["score"] = 0
91
+ state["streak"] = 0
92
+ state["correct_since_reward"] = 0
93
+ state["level_correct"] = 0
94
+ state["awaiting_reward"] = False
95
+ state["future"] = None
96
+
97
+ question, answer = generate_question(state["level"])
98
+ state["question"] = question
99
+ state["answer"] = answer
100
+
101
+ # Pre-generate first reward image immediately
102
+ _submit_generation(state)
103
+
104
+ return (state, gr.update(visible=False), gr.update(visible=True), "", _status_text(state), f"## {question} = ?")
105
+
106
+ except Exception as e:
107
+ print(f"start_game error: {e}")
108
+ return (state, gr.update(visible=True), gr.update(visible=False), f"⚠️ Something went wrong, try again.", "", "")
109
+
110
+
111
+ def check_answer(user_input: str, state: dict):
112
+ """Validate answer, update state, potentially show reward."""
113
+ import gradio as gr
114
+ try:
115
+ if not state or not state.get("question"):
116
+ return _no_change_outputs(state)
117
+
118
+ # Parse input
119
+ try:
120
+ user_answer = int(user_input.strip())
121
+ except (ValueError, AttributeError):
122
+ return (
123
+ state,
124
+ _status_text(state),
125
+ f"## {state['question']} = ?",
126
+ "",
127
+ "⚠️ Numbers only!",
128
+ None,
129
+ gr.update(visible=False),
130
+ )
131
 
132
+ correct = (user_answer == state["answer"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  reward_image = None
134
  reward_visible = False
135
+ feedback = ""
136
+
137
+ if correct:
138
+ state["score"] += 1
139
+ state["streak"] += 1
140
+ state["correct_since_reward"] += 1
141
+ state["level_correct"] += 1
142
+
143
+ # Check for level-up
144
+ threshold = LEVEL_THRESHOLDS.get(state["level"], 999)
145
+ level_msg = ""
146
+ if state["level_correct"] >= threshold and state["level"] < 4:
147
+ state["level"] += 1
148
+ state["level_correct"] = 0
149
+ level_msg = level_up_message(state["level"])
150
+
151
+ streak_fire = "πŸ”₯" * min(state["streak"], 5)
152
+ feedback = f"βœ… Great! {streak_fire}" if not level_msg else f"βœ… {level_msg}"
153
+
154
+ # Check if reward is due
155
+ if state["correct_since_reward"] >= REWARD_EVERY:
156
+ state["correct_since_reward"] = 0
157
+ future = state.get("future")
158
+
159
+ if future is not None and future.done():
160
+ # Image ready β€” grab it
161
+ try:
162
+ result = future.result()
163
+ reward_image = result[0] if result else None
164
+ except Exception as e:
165
+ print(f"Image retrieval error: {e}")
166
+ reward_image = None
167
+
168
+ # Kick off next generation
169
+ _submit_generation(state)
170
+ else:
171
+ # Still generating β€” mark as awaiting
172
+ state["awaiting_reward"] = True
173
+
174
+ reward_visible = True
175
+
176
+ else:
177
+ state["streak"] = 0
178
+ feedback = f"❌ Answer: **{state['answer']}**"
179
+
180
+ # If previously awaiting reward and image is now ready, grab it
181
+ if state.get("awaiting_reward") and not reward_visible:
182
+ future = state.get("future")
183
+ if future is not None and future.done():
184
+ try:
185
+ result = future.result()
186
+ reward_image = result[0] if result else None
187
+ except Exception as e:
188
+ print(f"Image retrieval error: {e}")
189
+ reward_image = None
190
+ state["awaiting_reward"] = False
191
+ _submit_generation(state)
192
+ reward_visible = True
193
 
194
  # Next question
195
  question, answer = generate_question(state["level"])
196
  state["question"] = question
197
+ state["answer"] = answer
 
 
 
 
 
 
198
 
199
  return (
200
  state,
201
  _status_text(state),
202
  f"## {question} = ?",
203
+ "", # clear input
204
  feedback,
205
  reward_image,
206
  gr.update(visible=reward_visible),
207
  )
208
 
209
+ except Exception as e:
210
+ print(f"check_answer error: {e}")
211
+ question, answer = generate_question(state.get("level", 1))
 
 
212
  state["question"] = question
213
+ state["answer"] = answer
214
  return (
215
  state,
216
  _status_text(state),
217
  f"## {question} = ?",
218
  "",
219
+ "⚠️ Something went wrong!",
220
  None,
221
  gr.update(visible=False),
222
  )
223
 
224
 
225
+ def restart(state: dict):
226
+ """Cancel any running future and go back to welcome screen."""
227
+ import gradio as gr
228
+ try:
229
+ future = state.get("future")
230
+ if future is not None:
231
+ future.cancel()
232
+ except Exception:
233
+ pass
234
+ return {}, gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), "", ""
 
 
 
 
 
 
 
 
 
 
 
235
 
236
  # ---------------------------------------------------------------------------
237
  # Gradio UI
238
  # ---------------------------------------------------------------------------
239
 
240
+ import gradio as gr
241
+
242
  CSS = """
243
+ #title { text-align: center; font-size: 2.2em; margin-bottom: 0.1em; }
244
+ #subtitle { text-align: center; color: #888; margin-bottom: 1em; font-style: italic; }
245
+ #question-box{ text-align: center; font-size: 2.6em; font-weight: bold; padding: 0.5em; }
246
+ #feedback-box{ text-align: center; font-size: 1.3em; min-height: 2em; }
247
+ #status-box { text-align: center; padding: 0.4em; border-radius: 8px; }
248
+ #reward-img { border-radius: 16px; }
249
  .answer-input input { font-size: 2em !important; text-align: center !important; }
250
+ .emoji-group label { font-size: 1.6em !important; }
251
  """
252
 
253
+ with gr.Blocks(title="πŸ’– LoveMath") as demo:
254
 
255
  state = gr.State({})
256
 
257
+ gr.Markdown("# πŸ’– LoveMath", elem_id="title")
258
+ gr.Markdown("*Do maths. Win cute animals!*", elem_id="subtitle")
 
259
 
260
+ # ── Welcome panel ──────────────────────────────────────────────────────
261
  with gr.Group(visible=True) as welcome_panel:
262
+ gr.Markdown("### Who are you?")
263
+ player_name = gr.Textbox(placeholder="Your name…", label="", max_lines=1)
264
+ next_btn = gr.Button("Next ➑️", variant="primary", size="lg")
265
+
266
+ # ── Emoji picker panel ─────────────────────────────────────────────────
267
+ with gr.Group(visible=False) as emoji_panel:
268
+ gr.Markdown("### Pick your animals 🐾")
269
+ animal_picker = gr.CheckboxGroup(
270
+ choices=ANIMAL_EMOJIS,
271
+ label="",
272
+ elem_classes=["emoji-group"],
273
+ )
274
+ gr.Markdown("### Pick your places 🌍")
275
+ place_picker = gr.CheckboxGroup(
276
+ choices=PLACE_EMOJIS,
277
+ label="",
278
+ elem_classes=["emoji-group"],
279
  )
280
+ picker_error = gr.Markdown("")
281
+ go_btn = gr.Button("Let's go! πŸš€", variant="primary", size="lg")
282
 
283
+ # ── Game panel ─────────────────────────────────────────────────────────
284
  with gr.Group(visible=False) as game_panel:
285
+ status_md = gr.Markdown("", elem_id="status-box", container=True)
 
 
286
  question_md = gr.Markdown("", elem_id="question-box")
287
 
288
  answer_input = gr.Textbox(
289
+ placeholder="?",
290
+ label="Your answer",
291
  max_lines=1,
292
  elem_classes=["answer-input"],
293
  )
294
+ check_btn = gr.Button("βœ”οΈ Check!", variant="primary")
 
295
  feedback_md = gr.Markdown("", elem_id="feedback-box")
296
 
 
297
  with gr.Group(visible=False) as reward_panel:
298
+ gr.Markdown("### 🎁 Your reward!")
299
  reward_image = gr.Image(
300
+ label="", show_label=False,
301
+ elem_id="reward-img", height=400,
 
 
302
  )
303
 
304
+ restart_btn = gr.Button("πŸ”„ Restart", variant="secondary", size="sm")
305
+
306
+ # ── Event wiring ───────────────────────────────────────────────────────
307
+
308
+ # Welcome β†’ Emoji picker
309
+ enter_outputs = [state, welcome_panel, emoji_panel, game_panel, animal_picker, place_picker]
310
+ next_btn.click(enter_name, inputs=[player_name, state], outputs=enter_outputs)
311
+ player_name.submit(enter_name, inputs=[player_name, state], outputs=enter_outputs)
312
+
313
+ # Emoji picker β†’ Game
314
+ start_outputs = [state, emoji_panel, game_panel, picker_error, status_md, question_md]
315
+ go_btn.click(start_game, inputs=[animal_picker, place_picker, state], outputs=start_outputs)
316
+
317
+ # Answer checking
318
+ check_outputs = [state, status_md, question_md, answer_input, feedback_md, reward_image, reward_panel]
319
+ check_btn.click(check_answer, inputs=[answer_input, state], outputs=check_outputs)
320
  answer_input.submit(check_answer, inputs=[answer_input, state], outputs=check_outputs)
321
 
322
+ # Restart
323
  restart_btn.click(
324
+ restart,
325
+ inputs=[state],
326
+ outputs=[state, welcome_panel, emoji_panel, game_panel, player_name, picker_error],
327
  )
328
 
329
 
330
  if __name__ == "__main__":
331
+ demo.launch(
332
+ css=CSS,
333
+ theme=gr.themes.Soft(primary_hue="purple", secondary_hue="pink"),
334
+ )
image_generator.py CHANGED
@@ -5,33 +5,46 @@ import torch
5
  # Patch for torch < 2.4 which lacks torch.xpu (required by diffusers >= 0.30)
6
  if not hasattr(torch, "xpu"):
7
  class _MockXPU:
8
- is_available = staticmethod(lambda: False)
9
- device_count = staticmethod(lambda: 0)
10
- empty_cache = staticmethod(lambda: None)
11
- manual_seed = staticmethod(lambda seed: None)
12
  reset_peak_memory_stats = staticmethod(lambda: None)
13
- max_memory_allocated = staticmethod(lambda: 0)
14
- synchronize = staticmethod(lambda: None)
15
  torch.xpu = _MockXPU()
16
 
17
  # ---------------------------------------------------------------------------
18
- # Detect if running on HuggingFace Spaces with ZeroGPU
19
  # ---------------------------------------------------------------------------
20
 
21
  IS_HF_SPACE = os.environ.get("SPACE_ID") is not None
22
-
23
  if IS_HF_SPACE:
24
  import spaces
25
 
26
  # ---------------------------------------------------------------------------
27
- # Animal reward prompts β€” escalate with streak
28
  # ---------------------------------------------------------------------------
29
 
30
- ANIMALS = [
31
- "bunny", "kitten", "puppy", "baby panda", "duckling",
32
- "baby fox", "hedgehog", "baby penguin", "baby koala",
33
- "baby deer", "hamster", "baby owl", "baby elephant",
34
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  STYLES = [
37
  "kawaii digital art",
@@ -40,33 +53,40 @@ STYLES = [
40
  "pastel chibi illustration",
41
  ]
42
 
43
- def build_prompt(streak: int) -> str:
44
- animal = random.choice(ANIMALS)
45
- style = random.choice(STYLES)
 
 
 
 
 
 
 
 
 
46
 
47
  if streak <= 2:
48
- mood = "cute"
49
  extras = "with big sparkling eyes, soft pastel colors"
50
  elif streak <= 5:
51
- mood = "super cute and happy"
52
  extras = "with big sparkling eyes, glitter, pastel rainbow colors, smiling"
53
  else:
54
- mood = "magically adorable, ultra fluffy"
55
  extras = (
56
  "with big sparkling eyes, magical sparkles, rainbow aura, "
57
  "tiny crown, pastel colors, looking amazed"
58
  )
59
 
60
  prompt = (
61
- f"A {mood} {animal}, {style}, {extras}, "
62
- "white background, children's book illustration style, "
63
- "high quality, detailed"
64
  )
65
  return prompt
66
 
67
-
68
  # ---------------------------------------------------------------------------
69
- # Pipeline loader (cached so we only load once)
70
  # ---------------------------------------------------------------------------
71
 
72
  _pipe = None
@@ -78,50 +98,56 @@ def get_pipeline():
78
 
79
  from diffusers import FluxPipeline
80
 
81
- print("Loading FLUX.1-schnell pipeline...")
82
  _pipe = FluxPipeline.from_pretrained(
83
  "black-forest-labs/FLUX.1-schnell",
84
  torch_dtype=torch.bfloat16,
85
  )
86
 
87
- # Device selection: CUDA > MPS (Apple Silicon) > CPU
88
  if torch.cuda.is_available():
89
  _pipe = _pipe.to("cuda")
90
  elif torch.backends.mps.is_available():
91
  _pipe = _pipe.to("mps")
92
  else:
93
- # CPU fallback β€” slow but works
94
  _pipe.enable_model_cpu_offload()
95
 
96
  print("Pipeline ready.")
97
  return _pipe
98
 
99
-
100
  # ---------------------------------------------------------------------------
101
- # Main generation function
102
- # Two versions: one with @spaces.GPU for HF, one plain for local
103
  # ---------------------------------------------------------------------------
104
 
105
- def _generate(streak: int):
106
- pipe = get_pipeline()
107
- prompt = build_prompt(streak)
108
- print(f"Generating image | streak={streak} | prompt: {prompt}")
109
- result = pipe(
110
- prompt=prompt,
111
- num_inference_steps=4, # schnell is optimised for 1-4 steps
112
- guidance_scale=0.0, # schnell doesn't use CFG
113
- height=512,
114
- width=512,
115
- )
116
- return result.images[0], prompt
 
 
 
 
 
 
117
 
118
 
 
 
 
 
119
  if IS_HF_SPACE:
120
  @spaces.GPU(duration=60)
121
- def generate_reward_image(streak: int):
122
- """Generate a cute animal image as a reward (HF Spaces / ZeroGPU)."""
123
- return _generate(streak)
124
  else:
125
- def generate_reward_image(streak: int):
126
- """Generate a cute animal image as a reward (local)."""
127
- return _generate(streak)
 
5
  # Patch for torch < 2.4 which lacks torch.xpu (required by diffusers >= 0.30)
6
  if not hasattr(torch, "xpu"):
7
  class _MockXPU:
8
+ is_available = staticmethod(lambda: False)
9
+ device_count = staticmethod(lambda: 0)
10
+ empty_cache = staticmethod(lambda: None)
11
+ manual_seed = staticmethod(lambda seed: None)
12
  reset_peak_memory_stats = staticmethod(lambda: None)
13
+ max_memory_allocated = staticmethod(lambda: 0)
14
+ synchronize = staticmethod(lambda: None)
15
  torch.xpu = _MockXPU()
16
 
17
  # ---------------------------------------------------------------------------
18
+ # Detect HuggingFace Spaces ZeroGPU
19
  # ---------------------------------------------------------------------------
20
 
21
  IS_HF_SPACE = os.environ.get("SPACE_ID") is not None
 
22
  if IS_HF_SPACE:
23
  import spaces
24
 
25
  # ---------------------------------------------------------------------------
26
+ # Emoji β†’ descriptive text maps (fed into FLUX prompt)
27
  # ---------------------------------------------------------------------------
28
 
29
+ ANIMAL_MAP: dict[str, str] = {
30
+ "🐢": "puppy", "🐱": "kitten", "🐰": "bunny",
31
+ "🦊": "baby fox", "🐼": "baby panda", "🐨": "baby koala",
32
+ "🦁": "baby lion", "🐯": "baby tiger", "🐸": "baby frog",
33
+ "🐧": "baby penguin", "πŸ¦‹": "butterfly", "πŸ¦„": "unicorn",
34
+ }
35
+
36
+ PLACE_MAP: dict[str, str] = {
37
+ "🌊": "on a sunny beach with ocean waves",
38
+ "πŸ”οΈ": "on a snowy mountain top",
39
+ "🌸": "in a cherry blossom garden",
40
+ "🌈": "under a rainbow",
41
+ "πŸŒ™": "on a glowing crescent moon",
42
+ "⭐": "surrounded by sparkling stars",
43
+ "🌴": "on a tropical island",
44
+ "🏑": "in a cosy cottage garden",
45
+ "🌺": "in a field of tropical flowers",
46
+ "πŸ„": "in an enchanted mushroom forest",
47
+ }
48
 
49
  STYLES = [
50
  "kawaii digital art",
 
53
  "pastel chibi illustration",
54
  ]
55
 
56
+ # ---------------------------------------------------------------------------
57
+ # Prompt builder
58
+ # ---------------------------------------------------------------------------
59
+
60
+ def build_prompt(streak: int, animals: list[str], places: list[str]) -> str:
61
+ # Pick one animal and one place from user selection
62
+ animal_emoji = random.choice(animals) if animals else random.choice(list(ANIMAL_MAP))
63
+ place_emoji = random.choice(places) if places else random.choice(list(PLACE_MAP))
64
+
65
+ animal_text = ANIMAL_MAP.get(animal_emoji, "bunny")
66
+ place_text = PLACE_MAP.get(place_emoji, "in a magical garden")
67
+ style = random.choice(STYLES)
68
 
69
  if streak <= 2:
70
+ mood = "cute"
71
  extras = "with big sparkling eyes, soft pastel colors"
72
  elif streak <= 5:
73
+ mood = "super cute and happy"
74
  extras = "with big sparkling eyes, glitter, pastel rainbow colors, smiling"
75
  else:
76
+ mood = "magically adorable, ultra fluffy"
77
  extras = (
78
  "with big sparkling eyes, magical sparkles, rainbow aura, "
79
  "tiny crown, pastel colors, looking amazed"
80
  )
81
 
82
  prompt = (
83
+ f"A {mood} {animal_text} {place_text}, {style}, {extras}, "
84
+ "white background, children's book illustration style, high quality, detailed"
 
85
  )
86
  return prompt
87
 
 
88
  # ---------------------------------------------------------------------------
89
+ # Pipeline loader (cached)
90
  # ---------------------------------------------------------------------------
91
 
92
  _pipe = None
 
98
 
99
  from diffusers import FluxPipeline
100
 
101
+ print("Loading FLUX.1-schnell pipeline…")
102
  _pipe = FluxPipeline.from_pretrained(
103
  "black-forest-labs/FLUX.1-schnell",
104
  torch_dtype=torch.bfloat16,
105
  )
106
 
 
107
  if torch.cuda.is_available():
108
  _pipe = _pipe.to("cuda")
109
  elif torch.backends.mps.is_available():
110
  _pipe = _pipe.to("mps")
111
  else:
 
112
  _pipe.enable_model_cpu_offload()
113
 
114
  print("Pipeline ready.")
115
  return _pipe
116
 
 
117
  # ---------------------------------------------------------------------------
118
+ # Core generation (always wrapped in try/except)
 
119
  # ---------------------------------------------------------------------------
120
 
121
+ def _generate(streak: int, animals: list[str], places: list[str]):
122
+ try:
123
+ pipe = get_pipeline()
124
+ prompt = build_prompt(streak, animals, places)
125
+ print(f"Generating | streak={streak} | prompt: {prompt}")
126
+
127
+ result = pipe(
128
+ prompt=prompt,
129
+ num_inference_steps=4,
130
+ guidance_scale=0.0,
131
+ height=512,
132
+ width=512,
133
+ )
134
+ return result.images[0], prompt
135
+
136
+ except Exception as e:
137
+ print(f"Image generation failed: {e}")
138
+ return None, ""
139
 
140
 
141
+ # ---------------------------------------------------------------------------
142
+ # Public API β€” two versions depending on environment
143
+ # ---------------------------------------------------------------------------
144
+
145
  if IS_HF_SPACE:
146
  @spaces.GPU(duration=60)
147
+ def generate_reward_image(streak: int, animals: list[str], places: list[str]):
148
+ """Generate reward image on HF Spaces ZeroGPU."""
149
+ return _generate(streak, animals, places)
150
  else:
151
+ def generate_reward_image(streak: int, animals: list[str], places: list[str]):
152
+ """Generate reward image locally."""
153
+ return _generate(streak, animals, places)
math_engine.py CHANGED
@@ -1,7 +1,7 @@
1
  import random
2
 
3
- # Levels definition: (operation_label, generator_function)
4
- # Targeting ages 9-10 (French CE2/CM1 curriculum)
5
 
6
  def generate_question(level: int) -> tuple[str, int]:
7
  """
@@ -19,7 +19,7 @@ def generate_question(level: int) -> tuple[str, int]:
19
 
20
  elif level == 2:
21
  a = random.randint(1, 30)
22
- b = random.randint(1, a) # ensure positive result
23
  return f"{a} - {b}", a - b
24
 
25
  elif level == 3:
@@ -45,17 +45,17 @@ def generate_question(level: int) -> tuple[str, int]:
45
 
46
  LEVEL_NAMES = {
47
  1: "βž• Additions",
48
- 2: "βž– Soustractions",
49
  3: "βœ–οΈ Multiplications",
50
- 4: "🎲 Mélange",
51
  }
52
 
53
- LEVEL_THRESHOLDS = {1: 5, 2: 5, 3: 5, 4: 999} # correct answers needed to level up
54
 
55
  def level_up_message(level: int) -> str:
56
  messages = {
57
- 2: "πŸŽ‰ Bravo ! Tu passes aux soustractions !",
58
- 3: "🌟 Incroyable ! Place aux multiplications !",
59
- 4: "πŸ† Tu es un(e) champion(ne) ! Mode expert activΓ© !",
60
  }
61
  return messages.get(level, "")
 
1
  import random
2
 
3
+ # Levels definition
4
+ # Targeting ages 9-10
5
 
6
  def generate_question(level: int) -> tuple[str, int]:
7
  """
 
19
 
20
  elif level == 2:
21
  a = random.randint(1, 30)
22
+ b = random.randint(1, a)
23
  return f"{a} - {b}", a - b
24
 
25
  elif level == 3:
 
45
 
46
  LEVEL_NAMES = {
47
  1: "βž• Additions",
48
+ 2: "βž– Subtractions",
49
  3: "βœ–οΈ Multiplications",
50
+ 4: "🎲 Mix",
51
  }
52
 
53
+ LEVEL_THRESHOLDS = {1: 5, 2: 5, 3: 5, 4: 999}
54
 
55
  def level_up_message(level: int) -> str:
56
  messages = {
57
+ 2: "πŸŽ‰ Next level!",
58
+ 3: "🌟 Multiplications!",
59
+ 4: "πŸ† Expert mode!",
60
  }
61
  return messages.get(level, "")