jefffffff9 Claude Sonnet 4.6 commited on
Commit
d351193
·
1 Parent(s): b8e51f4

Improve Bambara/Fula quality + add full correction UI

Browse files

voice_responder.py:
- Every template now returns (native_text, english_translation) tuple
- Added greeting/farewell/thanks/soil_ok/weather_ok/pest_ok templates
- Short MMS-TTS-optimised sentences kept (≤6 words)

intent_parser.py:
- Added greeting, thanks, farewell intents with Bambara/Fula/French/English keywords
- Added French and English keywords to agricultural intents

app.py:
- _run_pipeline returns (transcript, english_translation, response_text, audio)
- Tab 1: added English translation box; added "Send to Correction" button
- Tab 2: added English translation field + corrected_english field + corrected_response field
- "Send to Correction" auto-populates all Tab 2 fields from Tab 1 in one click
- _save_feedback_to_hub stores corrected_english and corrected_response

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. app.py +75 -30
  2. src/iot/intent_parser.py +36 -5
  3. src/iot/voice_responder.py +131 -31
app.py CHANGED
@@ -250,12 +250,12 @@ def _run_pipeline(audio_path: str, language_code: str):
250
  })
251
 
252
  responder = VoiceResponder(language=language_code)
253
- response_text = responder.generate_response(intent, sensor_data)
254
 
255
  # ── 3. MMS-TTS (GPU) ──────────────────────────────────────────────────────
256
  wav_np, sample_rate = _tts.synthesize(response_text, language_code, device=device)
257
 
258
- return transcript, response_text, (sample_rate, wav_np)
259
 
260
 
261
  # ── HF Hub feedback persistence ───────────────────────────────────────────────
@@ -264,7 +264,10 @@ def _save_feedback_to_hub(
264
  audio_path: str | None,
265
  transcript: str,
266
  corrected_text: str,
 
 
267
  response_text: str,
 
268
  rating: int,
269
  notes: str,
270
  language_label: str,
@@ -272,7 +275,7 @@ def _save_feedback_to_hub(
272
  language_code = SUPPORTED_LANGUAGES.get(language_label, "bam")
273
 
274
  if not corrected_text.strip():
275
- return "⚠️ Corrected text is empty."
276
 
277
  timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
278
 
@@ -283,7 +286,10 @@ def _save_feedback_to_hub(
283
  "audio_file": f"audio/{language_code}_{timestamp}.wav",
284
  "whisper_output": transcript,
285
  "corrected_text": corrected_text.strip(),
 
 
286
  "response_text": response_text,
 
287
  "rating": rating,
288
  "notes": notes.strip(),
289
  "is_correction": transcript.strip() != corrected_text.strip(),
@@ -424,19 +430,19 @@ def _get_adapter_status() -> str:
424
 
425
  def handle_ask(audio_path, language_label):
426
  if audio_path is None:
427
- return "⚠️ No audio — press Record or upload a file.", "", None
428
 
429
  language_code = SUPPORTED_LANGUAGES.get(language_label, "bam")
430
  status = _ensure_whisper_loaded()
431
 
432
  if _whisper_model is None:
433
- return f"⏳ Model loading ({status}). Wait a moment and try again.", "", None
434
 
435
  try:
436
- transcript, response_text, audio_out = _run_pipeline(audio_path, language_code)
437
- return transcript, response_text, audio_out
438
  except Exception as e:
439
- return f"❌ {e}", "", None
440
 
441
 
442
  # ── Gradio UI ─────────────────────────────────────────────────────────────────
@@ -458,10 +464,10 @@ def build_ui() -> gr.Blocks:
458
  status_timer = gr.Timer(value=3)
459
  status_timer.tick(fn=get_model_status, outputs=model_status_box)
460
 
461
- with gr.Tabs():
462
 
463
  # ── Tab 1: Voice Assistant ────────────────────────────────────────
464
- with gr.TabItem("🎙️ Voice Assistant"):
465
  with gr.Row():
466
  with gr.Column(scale=1):
467
  language_dd = gr.Dropdown(
@@ -478,14 +484,20 @@ def build_ui() -> gr.Blocks:
478
 
479
  with gr.Column(scale=1):
480
  transcript_box = gr.Textbox(
481
- label="Whisper heard",
482
- lines=3,
483
  placeholder="Your words will appear here…",
484
  interactive=False,
485
  )
 
 
 
 
 
 
486
  response_box = gr.Textbox(
487
- label="Response / Jaabi",
488
- lines=3,
489
  placeholder="Agricultural advice will appear here…",
490
  interactive=False,
491
  )
@@ -494,18 +506,23 @@ def build_ui() -> gr.Blocks:
494
  autoplay=True,
495
  interactive=False,
496
  )
 
 
 
 
 
497
 
498
  ask_btn.click(
499
  fn=handle_ask,
500
  inputs=[audio_input, language_dd],
501
- outputs=[transcript_box, response_box, audio_output],
502
  )
503
 
504
  # ── Tab 2: Feedback & Correction ─────────────────────────────────
505
- with gr.TabItem("📝 Feedback & Correction"):
506
  gr.Markdown(
507
- "Help improve the model by correcting transcription errors. "
508
- "Your audio and corrections are saved to the training dataset."
509
  )
510
  with gr.Row():
511
  with gr.Column():
@@ -517,35 +534,53 @@ def build_ui() -> gr.Blocks:
517
  fb_audio = gr.Audio(
518
  sources=["microphone", "upload"],
519
  type="filepath",
520
- label="Audio (re-record or upload)",
521
  )
 
522
  fb_transcript = gr.Textbox(
523
- label="Whisper output (what it heard)",
524
- lines=3,
525
- placeholder="Paste or type what Whisper said…",
526
  )
527
  fb_corrected = gr.Textbox(
528
- label="Corrected transcription (what was actually said)",
529
- lines=3,
530
- placeholder="Type the correct text here…",
531
  )
532
 
533
  with gr.Column():
 
 
 
 
 
 
 
 
 
 
 
 
534
  fb_response = gr.Textbox(
535
- label="Response text (optional — for rating)",
536
  lines=2,
537
- placeholder="Copy the response from Tab 1…",
 
 
 
 
 
538
  )
539
  fb_rating = gr.Slider(
540
  minimum=1, maximum=5, step=1, value=3,
541
- label="Response quality (1 = poor, 5 = excellent)",
542
  )
543
  fb_notes = gr.Textbox(
544
  label="Notes (optional)",
545
  lines=2,
546
  placeholder="e.g. noisy background, strong accent…",
547
  )
548
- save_btn = gr.Button("💾 Save to Dataset", variant="secondary")
549
  save_status = gr.Textbox(
550
  label="Save status", interactive=False, lines=2
551
  )
@@ -554,11 +589,21 @@ def build_ui() -> gr.Blocks:
554
  fn=_save_feedback_to_hub,
555
  inputs=[
556
  fb_audio, fb_transcript, fb_corrected,
557
- fb_response, fb_rating, fb_notes, fb_lang,
 
 
558
  ],
559
  outputs=[save_status],
560
  )
561
 
 
 
 
 
 
 
 
 
562
  # ── Tab 3: Training Status ────────────────────────────────────────
563
  with gr.TabItem("🔧 Training Status"):
564
  gr.Markdown(
 
250
  })
251
 
252
  responder = VoiceResponder(language=language_code)
253
+ response_text, english_translation = responder.generate_response(intent, sensor_data)
254
 
255
  # ── 3. MMS-TTS (GPU) ──────────────────────────────────────────────────────
256
  wav_np, sample_rate = _tts.synthesize(response_text, language_code, device=device)
257
 
258
+ return transcript, english_translation, response_text, (sample_rate, wav_np)
259
 
260
 
261
  # ── HF Hub feedback persistence ───────────────────────────────────────────────
 
264
  audio_path: str | None,
265
  transcript: str,
266
  corrected_text: str,
267
+ english_translation: str,
268
+ corrected_english: str,
269
  response_text: str,
270
+ corrected_response: str,
271
  rating: int,
272
  notes: str,
273
  language_label: str,
 
275
  language_code = SUPPORTED_LANGUAGES.get(language_label, "bam")
276
 
277
  if not corrected_text.strip():
278
+ return "⚠️ Corrected transcription is empty — please fill in what was actually said."
279
 
280
  timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
281
 
 
286
  "audio_file": f"audio/{language_code}_{timestamp}.wav",
287
  "whisper_output": transcript,
288
  "corrected_text": corrected_text.strip(),
289
+ "english_translation": english_translation.strip(),
290
+ "corrected_english": corrected_english.strip() or english_translation.strip(),
291
  "response_text": response_text,
292
+ "corrected_response": corrected_response.strip() or response_text.strip(),
293
  "rating": rating,
294
  "notes": notes.strip(),
295
  "is_correction": transcript.strip() != corrected_text.strip(),
 
430
 
431
  def handle_ask(audio_path, language_label):
432
  if audio_path is None:
433
+ return "⚠️ No audio — press Record or upload a file.", "", "", None
434
 
435
  language_code = SUPPORTED_LANGUAGES.get(language_label, "bam")
436
  status = _ensure_whisper_loaded()
437
 
438
  if _whisper_model is None:
439
+ return f"⏳ Model loading ({status}). Wait a moment and try again.", "", "", None
440
 
441
  try:
442
+ transcript, english_translation, response_text, audio_out = _run_pipeline(audio_path, language_code)
443
+ return transcript, english_translation, response_text, audio_out
444
  except Exception as e:
445
+ return f"❌ {e}", "", "", None
446
 
447
 
448
  # ── Gradio UI ─────────────────────────────────────────────────────────────────
 
464
  status_timer = gr.Timer(value=3)
465
  status_timer.tick(fn=get_model_status, outputs=model_status_box)
466
 
467
+ with gr.Tabs() as tabs:
468
 
469
  # ── Tab 1: Voice Assistant ────────────────────────────────────────
470
+ with gr.TabItem("🎙️ Voice Assistant", id="tab_voice"):
471
  with gr.Row():
472
  with gr.Column(scale=1):
473
  language_dd = gr.Dropdown(
 
484
 
485
  with gr.Column(scale=1):
486
  transcript_box = gr.Textbox(
487
+ label="Whisper heard (transcription)",
488
+ lines=2,
489
  placeholder="Your words will appear here…",
490
  interactive=False,
491
  )
492
+ translation_box = gr.Textbox(
493
+ label="English translation",
494
+ lines=2,
495
+ placeholder="English meaning will appear here…",
496
+ interactive=False,
497
+ )
498
  response_box = gr.Textbox(
499
+ label="Response in your language",
500
+ lines=2,
501
  placeholder="Agricultural advice will appear here…",
502
  interactive=False,
503
  )
 
506
  autoplay=True,
507
  interactive=False,
508
  )
509
+ correct_btn = gr.Button(
510
+ "✏️ Something wrong? Send to Correction tab",
511
+ variant="secondary",
512
+ size="sm",
513
+ )
514
 
515
  ask_btn.click(
516
  fn=handle_ask,
517
  inputs=[audio_input, language_dd],
518
+ outputs=[transcript_box, translation_box, response_box, audio_output],
519
  )
520
 
521
  # ── Tab 2: Feedback & Correction ─────────────────────────────────
522
+ with gr.TabItem("📝 Feedback & Correction", id="tab_feedback"):
523
  gr.Markdown(
524
+ "Correct what Whisper heard, the English translation, and the response. "
525
+ "All corrections are saved to the training dataset to improve future accuracy."
526
  )
527
  with gr.Row():
528
  with gr.Column():
 
534
  fb_audio = gr.Audio(
535
  sources=["microphone", "upload"],
536
  type="filepath",
537
+ label="Audio",
538
  )
539
+ gr.Markdown("**Step 1 — Fix the transcription**")
540
  fb_transcript = gr.Textbox(
541
+ label="What Whisper heard",
542
+ lines=2,
543
+ placeholder="Auto-filled from Tab 1…",
544
  )
545
  fb_corrected = gr.Textbox(
546
+ label="✏️ What was actually said (in Bambara/Fula)",
547
+ lines=2,
548
+ placeholder="Type the correct transcription here…",
549
  )
550
 
551
  with gr.Column():
552
+ gr.Markdown("**Step 2 — Fix the English translation**")
553
+ fb_english = gr.Textbox(
554
+ label="Auto-generated English translation",
555
+ lines=2,
556
+ placeholder="Auto-filled from Tab 1…",
557
+ )
558
+ fb_corrected_english = gr.Textbox(
559
+ label="✏️ Correct English translation",
560
+ lines=2,
561
+ placeholder="Type the correct English meaning here…",
562
+ )
563
+ gr.Markdown("**Step 3 — Fix the response**")
564
  fb_response = gr.Textbox(
565
+ label="Auto-generated response",
566
  lines=2,
567
+ placeholder="Auto-filled from Tab 1…",
568
+ )
569
+ fb_corrected_response = gr.Textbox(
570
+ label="✏️ Better response (in farmer's language)",
571
+ lines=2,
572
+ placeholder="Type a better response here…",
573
  )
574
  fb_rating = gr.Slider(
575
  minimum=1, maximum=5, step=1, value=3,
576
+ label="Overall quality (1 = poor, 5 = excellent)",
577
  )
578
  fb_notes = gr.Textbox(
579
  label="Notes (optional)",
580
  lines=2,
581
  placeholder="e.g. noisy background, strong accent…",
582
  )
583
+ save_btn = gr.Button("💾 Save to Dataset", variant="primary")
584
  save_status = gr.Textbox(
585
  label="Save status", interactive=False, lines=2
586
  )
 
589
  fn=_save_feedback_to_hub,
590
  inputs=[
591
  fb_audio, fb_transcript, fb_corrected,
592
+ fb_english, fb_corrected_english,
593
+ fb_response, fb_corrected_response,
594
+ fb_rating, fb_notes, fb_lang,
595
  ],
596
  outputs=[save_status],
597
  )
598
 
599
+ # Wire "Send to Correction" button — populates Tab 2 fields from Tab 1
600
+ correct_btn.click(
601
+ fn=lambda t, tr, r, lang: (t, t, tr, tr, r, r, lang),
602
+ inputs=[transcript_box, translation_box, response_box, language_dd],
603
+ outputs=[fb_transcript, fb_corrected, fb_english, fb_corrected_english,
604
+ fb_response, fb_corrected_response, fb_lang],
605
+ )
606
+
607
  # ── Tab 3: Training Status ────────────────────────────────────────
608
  with gr.TabItem("🔧 Training Status"):
609
  gr.Markdown(
src/iot/intent_parser.py CHANGED
@@ -18,25 +18,56 @@ class Intent:
18
 
19
  # Intent keyword taxonomy for Bambara (bam) and Fula (ful)
20
  INTENT_KEYWORDS: dict[str, dict[str, list[str]]] = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  "check_soil": {
22
  "bam": ["bunding", "nɔgɔ", "dugu", "foro", "sani"],
23
  "ful": ["leydi", "ngesa", "ladde"],
 
 
24
  },
25
  "check_weather": {
26
  "bam": ["teliman", "sanji", "dibi", "sira"],
27
- "ful": ["yeeso", "fuɗorde"],
 
 
28
  },
29
  "irrigation_status": {
30
- "bam": ["ji", "sanji", "foro"],
31
- "ful": ["ndiyam", "ngesa"],
 
 
32
  },
33
  "pest_alert": {
34
- "bam": ["kungoloni", "suruku"],
35
- "ful": ["biñ-biñ"],
 
 
36
  },
37
  }
38
 
39
  INTENT_ENTITIES = {
 
 
 
40
  "check_soil": "soil",
41
  "check_weather": "weather",
42
  "irrigation_status": "irrigation",
 
18
 
19
  # Intent keyword taxonomy for Bambara (bam) and Fula (ful)
20
  INTENT_KEYWORDS: dict[str, dict[str, list[str]]] = {
21
+ # Social / conversational
22
+ "greeting": {
23
+ "bam": ["i ni ce", "i ni sogoma", "i ni wula", "ani sogoma", "an ka"],
24
+ "ful": ["jam waali", "jam hiiri", "jam na", "no mbadda"],
25
+ "fr": ["bonjour", "bonsoir", "salut", "bonne nuit"],
26
+ "en": ["hello", "hi", "good morning", "good evening", "hey"],
27
+ },
28
+ "thanks": {
29
+ "bam": ["aw ni ce", "i ni ce barika", "a ni barika"],
30
+ "ful": ["jaraama", "barakallahu"],
31
+ "fr": ["merci", "je vous remercie"],
32
+ "en": ["thank", "thanks"],
33
+ },
34
+ "farewell": {
35
+ "bam": ["kana tɛmɛ", "i ka taa", "sini"],
36
+ "ful": ["yahdu jam", "o yahdu", "jemma"],
37
+ "fr": ["au revoir", "à bientôt", "bonne journée"],
38
+ "en": ["goodbye", "bye", "see you"],
39
+ },
40
+ # Agricultural
41
  "check_soil": {
42
  "bam": ["bunding", "nɔgɔ", "dugu", "foro", "sani"],
43
  "ful": ["leydi", "ngesa", "ladde"],
44
+ "fr": ["sol", "terre", "humidité"],
45
+ "en": ["soil", "ground", "moisture", "dirt"],
46
  },
47
  "check_weather": {
48
  "bam": ["teliman", "sanji", "dibi", "sira"],
49
+ "ful": ["yeeso", "fuɗorde", "ndiyam"],
50
+ "fr": ["météo", "temps", "pluie", "chaleur"],
51
+ "en": ["weather", "rain", "temperature", "hot"],
52
  },
53
  "irrigation_status": {
54
+ "bam": ["ji", "sanji", "foro ji"],
55
+ "ful": ["ndiyam", "ngesa ndiyam"],
56
+ "fr": ["irrigation", "arrosage", "eau"],
57
+ "en": ["irrigation", "water", "watering"],
58
  },
59
  "pest_alert": {
60
+ "bam": ["kungoloni", "suruku", "dɔgɔw"],
61
+ "ful": ["biñ-biñ", "kuuje"],
62
+ "fr": ["insecte", "nuisible", "ravageur"],
63
+ "en": ["pest", "insect", "bug"],
64
  },
65
  }
66
 
67
  INTENT_ENTITIES = {
68
+ "greeting": "social",
69
+ "thanks": "social",
70
+ "farewell": "social",
71
  "check_soil": "soil",
72
  "check_weather": "weather",
73
  "irrigation_status": "irrigation",
src/iot/voice_responder.py CHANGED
@@ -1,7 +1,8 @@
1
  """
2
  Generates voice response text from sensor data in the farmer's own language.
3
  Supports Bambara (bam), Fula (ful), French (fr), and English (en).
4
- Bambara/Fula templates use short sentences (≤15 words) for best MMS-TTS quality.
 
5
  """
6
  from __future__ import annotations
7
 
@@ -20,55 +21,140 @@ TEMP_HIGH = 38.0
20
  PEST_ALERT_HIGH = 2 # Alert level ≥ 2 → warning
21
 
22
  # ── Bambara templates (≤6 words per sentence for clear MMS-TTS output) ───────
23
- BAMBARA_TEMPLATES = {
24
- "soil_moisture_low": "Bunding ji dɔgɔ. I ka foro ji.",
25
- "soil_moisture_high": "Ji ca kojugu. Foro ma fɛ.",
26
- "soil_ph_low": "Bunding kɔnɔ jugu. Kalisi fara a kan.",
27
- "soil_ph_high": "Bunding kɔnɔ tɛmɛ. Soufre fara a kan.",
28
- "weather_hot": "Teliman gbɛlɛ. Tile ma sigi.",
29
- "rain_likely": "Sanji bɛ na. Sɔrɔ .",
30
- "pest_high": "Dɔgɔw foro kɔnɔ. Bɔ u.",
31
- "irrigation_needed": "Foro fɛ ji. Ji sira yɔrɔ.",
32
- "irrigation_active": "Ji taa. A kɛ cogo di.",
33
- "default": "Kabako jumanw sɔrɔla.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
 
36
  # ── Fula templates (≤6 words per sentence for clear MMS-TTS output) ──────────
37
- FULA_TEMPLATES = {
38
- "soil_moisture_low": "Leydi ndiyam famɗi. Wado ngesa.",
39
- "soil_moisture_high": "Ndiyam heewi. Leydi famɗaali.",
40
- "soil_ph_low": "Leydi suurii. Waɗ kalisi.",
41
- "soil_ph_high": "Leydi alkalii. Waɗ soufre.",
42
- "weather_hot": "Nguleeki heewi. Muusal.",
43
- "rain_likely": "Ndiyam wadata. Loosu ngesa.",
44
- "pest_high": "Biñ-biñ ngesa nder. Fiil ɗen.",
45
- "irrigation_needed": "Ngesa fɛɗɛli ndiyam. Wado.",
46
- "irrigation_active": "Ndiyam wona jooni.",
47
- "default": "Humpito juuti waɗaama.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
 
50
 
51
  class VoiceResponder:
52
- """Converts sensor readings into actionable voice messages in the farmer's language."""
 
 
 
 
53
 
54
  def __init__(self, language: str = "fr") -> None:
55
  self.language = language
56
 
57
- def generate_response(self, intent: "Intent", sensor_data: "SensorData") -> str:
 
58
  if self.language == "bam":
59
- return self._bambara_response(sensor_data)
60
  elif self.language == "ful":
61
- return self._fula_response(sensor_data)
62
  else:
63
- return self._french_response(sensor_data)
 
64
 
65
  # ── Bambara ──────────────────────────────────────────────────────────────
66
 
67
- def _bambara_response(self, sensor_data: "SensorData") -> str:
68
  t = sensor_data.sensor_type
69
  v = sensor_data.values
70
  T = BAMBARA_TEMPLATES
71
 
 
 
 
 
 
 
 
 
 
72
  if t == "soil":
73
  moisture = v.get("moisture_pct")
74
  if moisture is not None:
@@ -82,6 +168,7 @@ class VoiceResponder:
82
  return T["soil_ph_low"]
83
  elif ph > SOIL_PH_HIGH:
84
  return T["soil_ph_high"]
 
85
 
86
  elif t == "weather":
87
  temp = v.get("temperature_c")
@@ -90,10 +177,11 @@ class VoiceResponder:
90
  return T["weather_hot"]
91
  if rain is not None and rain > 70:
92
  return T["rain_likely"]
 
93
 
94
  elif t == "irrigation":
95
- last = v.get("last_irrigation_h_ago")
96
  active = v.get("active")
 
97
  if active:
98
  return T["irrigation_active"]
99
  if last is not None and last > 24:
@@ -103,16 +191,25 @@ class VoiceResponder:
103
  level = int(v.get("alert_level", 0))
104
  if level >= PEST_ALERT_HIGH:
105
  return T["pest_high"]
 
106
 
107
  return T["default"]
108
 
109
  # ── Fula ─────────────────────────────────────────────────────────────────
110
 
111
- def _fula_response(self, sensor_data: "SensorData") -> str:
112
  t = sensor_data.sensor_type
113
  v = sensor_data.values
114
  T = FULA_TEMPLATES
115
 
 
 
 
 
 
 
 
 
116
  if t == "soil":
117
  moisture = v.get("moisture_pct")
118
  if moisture is not None:
@@ -126,6 +223,7 @@ class VoiceResponder:
126
  return T["soil_ph_low"]
127
  elif ph > SOIL_PH_HIGH:
128
  return T["soil_ph_high"]
 
129
 
130
  elif t == "weather":
131
  temp = v.get("temperature_c")
@@ -134,6 +232,7 @@ class VoiceResponder:
134
  return T["weather_hot"]
135
  if rain is not None and rain > 70:
136
  return T["rain_likely"]
 
137
 
138
  elif t == "irrigation":
139
  active = v.get("active")
@@ -147,6 +246,7 @@ class VoiceResponder:
147
  level = int(v.get("alert_level", 0))
148
  if level >= PEST_ALERT_HIGH:
149
  return T["pest_high"]
 
150
 
151
  return T["default"]
152
 
 
1
  """
2
  Generates voice response text from sensor data in the farmer's own language.
3
  Supports Bambara (bam), Fula (ful), French (fr), and English (en).
4
+ Bambara/Fula templates use short sentences (≤6 words) for best MMS-TTS quality.
5
+ Each template has an English equivalent used for the translation display in the UI.
6
  """
7
  from __future__ import annotations
8
 
 
21
  PEST_ALERT_HIGH = 2 # Alert level ≥ 2 → warning
22
 
23
  # ── Bambara templates (≤6 words per sentence for clear MMS-TTS output) ───────
24
+ # Format: (bambara_text, english_translation)
25
+ BAMBARA_TEMPLATES: dict[str, tuple[str, str]] = {
26
+ # Greetings / social
27
+ "greeting": ("I ni ce. N i dɛmɛ.",
28
+ "Hello. I am here to help you."),
29
+ "greeting_morning": ("I ni sogoma. Sɔrɔ ka ɲi.",
30
+ "Good morning. May the harvest be good."),
31
+ "greeting_evening": ("I ni wula. Kɛnɛya.",
32
+ "Good evening. Good health to you."),
33
+ "thanks": ("Aw ni ce. A ka ɲi.",
34
+ "Thank you. That is good."),
35
+ "farewell": ("Kana tɛmɛ. Ne bɛ i kɔnɔ.",
36
+ "Goodbye. I will be thinking of you."),
37
+ "not_understood": ("N ma a faamu. A fɔ.",
38
+ "I did not understand. Please repeat."),
39
+ # Soil
40
+ "soil_moisture_low": ("Bunding ji dɔgɔ. I ka foro ji.",
41
+ "Soil moisture is low. Irrigate your field."),
42
+ "soil_moisture_high": ("Ji ca kojugu. Foro ma fɛ.",
43
+ "Too much water. The field is flooded."),
44
+ "soil_ph_low": ("Bunding kɔnɔ jugu. Kalisi fara a kan.",
45
+ "Soil is too acidic. Apply lime."),
46
+ "soil_ph_high": ("Bunding kɔnɔ tɛmɛ. Soufre fara a kan.",
47
+ "Soil is too alkaline. Apply sulphur."),
48
+ "soil_ok": ("Bunding ka ɲi. Foro sɔrɔ.",
49
+ "Soil conditions are good. The field is ready."),
50
+ # Weather
51
+ "weather_hot": ("Teliman gbɛlɛ. Tile ma sigi.",
52
+ "It is very hot. Do not work in the midday sun."),
53
+ "rain_likely": ("Sanji bɛ na. Sɔrɔ jɔ.",
54
+ "Rain is coming. Protect the harvest."),
55
+ "weather_ok": ("Yɔrɔ min ɲi. Kɛ.",
56
+ "Weather is fine. You can work."),
57
+ # Pest
58
+ "pest_high": ("Dɔgɔw bɛ foro kɔnɔ. Bɔ u.",
59
+ "Pests are in the field. Drive them out."),
60
+ "pest_ok": ("Dɔgɔw tɛ yen. Foro ka ɲi.",
61
+ "No pests detected. The field looks healthy."),
62
+ # Irrigation
63
+ "irrigation_needed": ("Foro fɛ ji. Ji sira yɔrɔ.",
64
+ "The field needs water. Open the irrigation."),
65
+ "irrigation_active": ("Ji bɛ taa. A bɛ kɛ cogo di.",
66
+ "Irrigation is running. It is working well."),
67
+ # Default
68
+ "default": ("Kabako jumanw sɔrɔla.",
69
+ "Sensor data received. No alerts at this time."),
70
  }
71
 
72
  # ── Fula templates (≤6 words per sentence for clear MMS-TTS output) ──────────
73
+ # Format: (fula_text, english_translation)
74
+ FULA_TEMPLATES: dict[str, tuple[str, str]] = {
75
+ # Greetings / social
76
+ "greeting": ("Jam waali. Mi woni ɗoo.",
77
+ "Hello. I am here."),
78
+ "greeting_morning": ("Jam waali. Yoo barke.",
79
+ "Good morning. May there be blessings."),
80
+ "greeting_evening": ("Jam hiiri. Jam waɗaa.",
81
+ "Good evening. Peace be with you."),
82
+ "thanks": ("Jaraama. A weli.",
83
+ "Thank you. That is good."),
84
+ "farewell": ("Yahdu jam. Mi anndii.",
85
+ "Go in peace. I will remember you."),
86
+ "not_understood": ("Mi faamii wanaa. Hol ɗoo.",
87
+ "I did not understand. Please say again."),
88
+ # Soil
89
+ "soil_moisture_low": ("Leydi ndiyam famɗi. Wado ngesa.",
90
+ "Soil moisture is low. Water the field."),
91
+ "soil_moisture_high": ("Ndiyam heewi. Leydi famɗaali.",
92
+ "Too much water. Drainage is needed."),
93
+ "soil_ph_low": ("Leydi suurii. Waɗ kalisi.",
94
+ "Soil is acidic. Add lime."),
95
+ "soil_ph_high": ("Leydi alkalii. Waɗ soufre.",
96
+ "Soil is alkaline. Add sulphur."),
97
+ "soil_ok": ("Leydi weli. Ngesa yoodi.",
98
+ "Soil is good. The field is ready."),
99
+ # Weather
100
+ "weather_hot": ("Nguleeki heewi. Muusal.",
101
+ "It is very hot. Rest during midday."),
102
+ "rain_likely": ("Ndiyam wadata. Loosu ngesa.",
103
+ "Rain is coming. Protect the harvest."),
104
+ "weather_ok": ("Jawdi weli. Waɗ golle.",
105
+ "Weather is fine. Go work."),
106
+ # Pest
107
+ "pest_high": ("Biñ-biñ ngesa nder. Fiil ɗen.",
108
+ "Pests are in the field. Remove them."),
109
+ "pest_ok": ("Biñ-biñ alaa. Ngesa weli.",
110
+ "No pests found. Field looks healthy."),
111
+ # Irrigation
112
+ "irrigation_needed": ("Ngesa fɛɗɛli ndiyam. Wado.",
113
+ "Field needs water. Start irrigation."),
114
+ "irrigation_active": ("Ndiyam wona jooni.",
115
+ "Irrigation is running now."),
116
+ # Default
117
+ "default": ("Humpito juuti waɗaama.",
118
+ "Sensor data received. No alerts."),
119
  }
120
 
121
 
122
  class VoiceResponder:
123
+ """Converts sensor readings into actionable voice messages in the farmer's language.
124
+
125
+ generate_response() returns (native_text, english_translation).
126
+ For French/English, english_translation == native_text.
127
+ """
128
 
129
  def __init__(self, language: str = "fr") -> None:
130
  self.language = language
131
 
132
+ def generate_response(self, intent: "Intent", sensor_data: "SensorData") -> tuple[str, str]:
133
+ """Return (response_in_native_language, english_translation)."""
134
  if self.language == "bam":
135
+ return self._bambara_response(intent, sensor_data)
136
  elif self.language == "ful":
137
+ return self._fula_response(intent, sensor_data)
138
  else:
139
+ text = self._french_response(sensor_data)
140
+ return text, text
141
 
142
  # ── Bambara ──────────────────────────────────────────────────────────────
143
 
144
+ def _bambara_response(self, intent: "Intent", sensor_data: "SensorData") -> tuple[str, str]:
145
  t = sensor_data.sensor_type
146
  v = sensor_data.values
147
  T = BAMBARA_TEMPLATES
148
 
149
+ # Greeting intents — checked first regardless of sensor type
150
+ if intent.action == "greeting":
151
+ key = "greeting_morning" if v.get("is_morning") else "greeting"
152
+ return T[key]
153
+ if intent.action == "thanks":
154
+ return T["thanks"]
155
+ if intent.action == "farewell":
156
+ return T["farewell"]
157
+
158
  if t == "soil":
159
  moisture = v.get("moisture_pct")
160
  if moisture is not None:
 
168
  return T["soil_ph_low"]
169
  elif ph > SOIL_PH_HIGH:
170
  return T["soil_ph_high"]
171
+ return T["soil_ok"]
172
 
173
  elif t == "weather":
174
  temp = v.get("temperature_c")
 
177
  return T["weather_hot"]
178
  if rain is not None and rain > 70:
179
  return T["rain_likely"]
180
+ return T["weather_ok"]
181
 
182
  elif t == "irrigation":
 
183
  active = v.get("active")
184
+ last = v.get("last_irrigation_h_ago")
185
  if active:
186
  return T["irrigation_active"]
187
  if last is not None and last > 24:
 
191
  level = int(v.get("alert_level", 0))
192
  if level >= PEST_ALERT_HIGH:
193
  return T["pest_high"]
194
+ return T["pest_ok"]
195
 
196
  return T["default"]
197
 
198
  # ── Fula ─────────────────────────────────────────────────────────────────
199
 
200
+ def _fula_response(self, intent: "Intent", sensor_data: "SensorData") -> tuple[str, str]:
201
  t = sensor_data.sensor_type
202
  v = sensor_data.values
203
  T = FULA_TEMPLATES
204
 
205
+ # Greeting intents — checked first regardless of sensor type
206
+ if intent.action == "greeting":
207
+ return T["greeting"]
208
+ if intent.action == "thanks":
209
+ return T["thanks"]
210
+ if intent.action == "farewell":
211
+ return T["farewell"]
212
+
213
  if t == "soil":
214
  moisture = v.get("moisture_pct")
215
  if moisture is not None:
 
223
  return T["soil_ph_low"]
224
  elif ph > SOIL_PH_HIGH:
225
  return T["soil_ph_high"]
226
+ return T["soil_ok"]
227
 
228
  elif t == "weather":
229
  temp = v.get("temperature_c")
 
232
  return T["weather_hot"]
233
  if rain is not None and rain > 70:
234
  return T["rain_likely"]
235
+ return T["weather_ok"]
236
 
237
  elif t == "irrigation":
238
  active = v.get("active")
 
246
  level = int(v.get("alert_level", 0))
247
  if level >= PEST_ALERT_HIGH:
248
  return T["pest_high"]
249
+ return T["pest_ok"]
250
 
251
  return T["default"]
252