sxandie commited on
Commit
67181d4
Β·
1 Parent(s): c225171

feat: unified Wilderness AI chatbot & 1s GPS updates with proximity highlights

Browse files
Files changed (6) hide show
  1. README.md +14 -9
  2. app.py +40 -32
  3. assets/custom.css +5 -0
  4. hackathon_submission_article.md +6 -3
  5. requirements.txt +6 -6
  6. src/llm.py +34 -24
README.md CHANGED
@@ -60,15 +60,20 @@ graph TD
60
  * **Live HUD Dashboard:** Telemetry tracking route completion percentage, cumulative distance hiked, current altitude, and next-checkpoint ETA.
61
  * **Offline Proximity Alerts:** Audio-visual indicators triggered automatically when the hiker is within 150m of any filtered POI.
62
 
63
- ### 3. Contextual Wilderness Guide AI & First-Aid RAG
64
- * **Wilderness First-Aid Manual:** Formulated manual (`first_aid_guide.json`) containing 5 key backcountry sections (bleeding, hypothermia, heat stroke, altitude illness, musculoskeletal injuries).
65
- * **Keyword RAG Search:** Local keyword intersection retriever indexes the guide and returns relevant instructions citing specific manual sections.
66
- * **In-Process LLM:** Powered by local GGUF models running via `llama-cpp-python`.
67
- * **Proximity Checkpoint Narration:** Provides terrain updates, safety advice, and target destination briefings as you approach checkpoints.
68
-
69
- ### 4. Offline Voice Journal & Post-Trek Reports
70
- * **ASR Voice Logs:** Dictate logs hands-free in the cold using `pywhispercpp` (whisper.cpp tiny). Logs transcribing audio, time, and coordinates are saved directly to SQLite.
71
- * **Post-Trek Storyteller:** Converts your journal entries and raw GPS points into an AI-narrated story artifact.
 
 
 
 
 
72
 
73
  ---
74
 
 
60
  * **Live HUD Dashboard:** Telemetry tracking route completion percentage, cumulative distance hiked, current altitude, and next-checkpoint ETA.
61
  * **Offline Proximity Alerts:** Audio-visual indicators triggered automatically when the hiker is within 150m of any filtered POI.
62
 
63
+ ### 3. Unified Wilderness Guide & First-Aid AI
64
+ * **Unified Chatbot Interface:** Merges the **Wilderness Guide AI** and **Wilderness First-Aid manual** query engine into a single chatbot interface.
65
+ * **Emergency Quick-Lookup:** Column layout integrates a sidebar with the offline Emergency Card and a Manual Quick Search accordion for instant access.
66
+ * **Robust Local LLM Processing:** Configured with a `120s` timeout threshold to prevent premature mock fallback during heavy local prompt prefilling.
67
+ * **Keyword RAG Search:** Local keyword intersection retriever indexes the manual (`first_aid_guide.json`) and guides the local `gemma-2b-it` LLM model to return highly grounded first-aid instructions with manual citations.
68
+ * **Proximity Checkpoint Narration:** Delivers terrain updates, safety advice, and target destination briefings as hikers approach landmarks.
69
+
70
+ ### 4. Live GPS Tracking & 1s Updates
71
+ * **1-Second Updates:** Configured GPS tracking refresh interval to 1s, enabling high-resolution position updates on the trail.
72
+ * **Active Proximity POI Highlights:** Automatically detects and highlights close Points of Interest (POIs) near the hiker's current coordinates in the Active Proximity Alerts panel.
73
+
74
+ ### 5. Offline Voice Journal & Post-Trek Reports
75
+ * **ASR Voice Logs:** Dictate logs hands-free in the cold using whisper.cpp tiny. Logs transcribing audio, time, and coordinates are saved directly to SQLite.
76
+ * **Post-Trek Storyteller:** Converts journal entries and raw GPS points into an engaging, non-technical first-person narrative (optimized for social media sharing) without listing raw coordinates.
77
 
78
  ---
79
 
app.py CHANGED
@@ -25,6 +25,17 @@ MAP_HTML_INITIALIZER = """
25
 
26
  MAP_INIT_JS = r"""
27
  () => {
 
 
 
 
 
 
 
 
 
 
 
28
  // 1. Dynamically append Leaflet CSS
29
  if (!document.getElementById("leaflet-css")) {
30
  var link = document.createElement("link");
@@ -386,7 +397,7 @@ MAP_INIT_JS = r"""
386
  window.gpsWatchId = navigator.geolocation.watchPosition(
387
  function(position) {
388
  var now = Date.now();
389
- if (now - window.lastGpsTime < 5000) return;
390
  window.lastGpsTime = now;
391
 
392
  var lat = position.coords.latitude;
@@ -417,7 +428,7 @@ MAP_INIT_JS = r"""
417
  },
418
  {
419
  enableHighAccuracy: true,
420
- maximumAge: 5000,
421
  timeout: 15000
422
  }
423
  );
@@ -1429,10 +1440,9 @@ def handle_generate_story(route_state_val, style):
1429
  f"Format Style: {style_instruction}\n"
1430
  "Emphasize the hiker's voice notes, detailing their personal reflections, physical state, and "
1431
  "wilderness observations. Incorporate the trek details (distance, elevation, altitude) to frame the physical challenge. "
1432
- "Weave in the amenities (water sources, campsites, alpine huts, shelters, viewpoints) as milestones or locations where the hiker is resting, "
1433
- "refilling water, or finding shelter.\n"
1434
  "Do not invent external landmarks, voice notes, or major events not provided. Keep the tone rugged, epic, and highly tactical. "
1435
- "Organize the story using headings corresponding to distance milestones.\n"
1436
  "At the end, sign off as 'Trailhead AI Storyteller'."
1437
  )
1438
 
@@ -1464,9 +1474,9 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead β€” Tactical Trail Comp
1464
  route_state = gr.State(None)
1465
  null_state = gr.State(None)
1466
  current_point_idx = gr.State(0)
1467
- hiker_pos_coords = gr.Textbox(visible=False, elem_id="hiker-pos-coords")
1468
- live_gps_coords = gr.Textbox(visible=False, elem_id="live-gps-coords")
1469
- route_data_json = gr.Textbox(visible=False, elem_id="route-data-json")
1470
 
1471
  hiker_pos_coords.change(
1472
  fn=None,
@@ -1583,33 +1593,31 @@ with gr.Blocks(css="assets/custom.css", title="Trailhead β€” Tactical Trail Comp
1583
  col_count=(4, "fixed")
1584
  )
1585
 
1586
- with gr.TabItem("🩺 Wilderness First-Aid"):
1587
  with gr.Row():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1588
  with gr.Column(scale=1):
1589
  gr.HTML(EMERGENCY_CARD)
1590
- with gr.Column(scale=1):
1591
- gr.Markdown("## πŸ” Wilderness First-Aid manual RAG Search")
1592
- rag_query = gr.Textbox(placeholder="What symptoms or injury do you want to query?", label="Query Symptoms")
1593
- rag_search_btn = gr.Button("Search manual", variant="primary")
1594
- rag_output = gr.Markdown(value="*Manual results will be displayed here.*")
1595
-
1596
- with gr.TabItem("πŸ’¬ Wilderness Guide AI"):
1597
- # Dynamically configure chatbot to use "messages" type if on Gradio 5
1598
- gradio_version = getattr(gr, "__version__", "5.0.0")
1599
- if gradio_version.startswith("6"):
1600
- chatbot_component = gr.Chatbot()
1601
- else:
1602
- chatbot_component = gr.Chatbot(type="messages")
1603
-
1604
- gr.ChatInterface(
1605
- respond,
1606
- chatbot=chatbot_component,
1607
- examples=[
1608
- "What gear checklist do I need for a 3-day high-altitude trek?",
1609
- "How do I treat a sprained ankle on the trail?",
1610
- "What is Naismith's Rule for calculating hiking time?"
1611
- ]
1612
- )
1613
 
1614
  with gr.TabItem("πŸŽ™οΈ Voice Journal & Reports"):
1615
  with gr.Row():
 
25
 
26
  MAP_INIT_JS = r"""
27
  () => {
28
+ // Hide communication textboxes instantly
29
+ var hideElements = function() {
30
+ ['hiker-pos-coords', 'live-gps-coords', 'route-data-json'].forEach(function(id) {
31
+ var el = document.getElementById(id);
32
+ if (el) el.style.setProperty("display", "none", "important");
33
+ });
34
+ };
35
+ hideElements();
36
+ var hideInterval = setInterval(hideElements, 50);
37
+ setTimeout(function() { clearInterval(hideInterval); }, 4000);
38
+
39
  // 1. Dynamically append Leaflet CSS
40
  if (!document.getElementById("leaflet-css")) {
41
  var link = document.createElement("link");
 
397
  window.gpsWatchId = navigator.geolocation.watchPosition(
398
  function(position) {
399
  var now = Date.now();
400
+ if (now - window.lastGpsTime < 1000) return;
401
  window.lastGpsTime = now;
402
 
403
  var lat = position.coords.latitude;
 
428
  },
429
  {
430
  enableHighAccuracy: true,
431
+ maximumAge: 1000,
432
  timeout: 15000
433
  }
434
  );
 
1440
  f"Format Style: {style_instruction}\n"
1441
  "Emphasize the hiker's voice notes, detailing their personal reflections, physical state, and "
1442
  "wilderness observations. Incorporate the trek details (distance, elevation, altitude) to frame the physical challenge. "
1443
+ "Synthesize the encountered amenities (water sources, campsites, alpine huts, shelters, viewpoints) naturally in a non-technical, "
1444
+ "cohesive narrative flow rather than listing every single route coordinate or waypoint. Do not list every milestone or amenity one-by-one. "
1445
  "Do not invent external landmarks, voice notes, or major events not provided. Keep the tone rugged, epic, and highly tactical. "
 
1446
  "At the end, sign off as 'Trailhead AI Storyteller'."
1447
  )
1448
 
 
1474
  route_state = gr.State(None)
1475
  null_state = gr.State(None)
1476
  current_point_idx = gr.State(0)
1477
+ hiker_pos_coords = gr.Textbox(visible=True, elem_id="hiker-pos-coords")
1478
+ live_gps_coords = gr.Textbox(visible=True, elem_id="live-gps-coords")
1479
+ route_data_json = gr.Textbox(visible=True, elem_id="route-data-json")
1480
 
1481
  hiker_pos_coords.change(
1482
  fn=None,
 
1593
  col_count=(4, "fixed")
1594
  )
1595
 
1596
+ with gr.TabItem("πŸ’¬ Wilderness Guide & First-Aid AI"):
1597
  with gr.Row():
1598
+ with gr.Column(scale=2):
1599
+ # Dynamically configure chatbot to use "messages" type if on Gradio 5
1600
+ gradio_version = getattr(gr, "__version__", "5.0.0")
1601
+ if gradio_version.startswith("6"):
1602
+ chatbot_component = gr.Chatbot()
1603
+ else:
1604
+ chatbot_component = gr.Chatbot(type="messages")
1605
+
1606
+ gr.ChatInterface(
1607
+ respond,
1608
+ chatbot=chatbot_component,
1609
+ examples=[
1610
+ "What gear checklist do I need for a 3-day high-altitude trek?",
1611
+ "How do I treat a sprained ankle on the trail?",
1612
+ "What is Naismith's Rule for calculating hiking time?"
1613
+ ]
1614
+ )
1615
  with gr.Column(scale=1):
1616
  gr.HTML(EMERGENCY_CARD)
1617
+ with gr.Accordion("πŸ” First-Aid Manual Quick Search", open=False):
1618
+ rag_query = gr.Textbox(placeholder="What symptoms or injury do you want to query?", label="Query Symptoms")
1619
+ rag_search_btn = gr.Button("Search manual", variant="primary")
1620
+ rag_output = gr.Markdown(value="*Manual results will be displayed here.*")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1621
 
1622
  with gr.TabItem("πŸŽ™οΈ Voice Journal & Reports"):
1623
  with gr.Row():
assets/custom.css CHANGED
@@ -156,3 +156,8 @@ p, span, label {
156
  letter-spacing: 0.1em;
157
  margin-top: 5px;
158
  }
 
 
 
 
 
 
156
  letter-spacing: 0.1em;
157
  margin-top: 5px;
158
  }
159
+
160
+ /* Hide communication textboxes from the layout while keeping them active in the DOM */
161
+ #hiker-pos-coords, #live-gps-coords, #route-data-json {
162
+ display: none !important;
163
+ }
hackathon_submission_article.md CHANGED
@@ -78,10 +78,13 @@ To prevent the 2B model from hallucinating medical advice in life-or-death scena
78
  Trailhead parses GPX files, corrects noisy elevation data using a moving average and a 2.0-meter minimum threshold, and accurately predicts trek times using Naismith's Rule.
79
 
80
  ### 2. πŸ—ΊοΈ Tactical HUD & Interactive Mapping
81
- The frontend is a completely custom, mobile-optimized Gradio interface featuring a native Leaflet canvas. It supports live GPS tracking, simulated trek playback, and renders POI markers without needing external map tile fetches on the trail.
82
 
83
- ### 3. πŸŽ™οΈ Geotagged Voice Journal & Storyteller
84
- Hikers can log voice entries while hiking. The application transcribes the audio, stamps it with the current GPS coordinates and altitude, and stores it in an SQLite database. Post-trek, the LLM compiles these logs, stats, and POI encounters into an engaging, shareable expedition report.
 
 
 
85
 
86
  ---
87
 
 
78
  Trailhead parses GPX files, corrects noisy elevation data using a moving average and a 2.0-meter minimum threshold, and accurately predicts trek times using Naismith's Rule.
79
 
80
  ### 2. πŸ—ΊοΈ Tactical HUD & Interactive Mapping
81
+ The frontend is a completely custom, mobile-optimized Gradio interface featuring a native Leaflet canvas. It supports live GPS tracking with high-frequency 1-second refresh intervals, simulated trek playback, active proximity alert updates, and highlights POI markers near the hiker's current position on the map without needing external map tile fetches on the trail.
82
 
83
+ ### 3. πŸ’¬ Unified Wilderness AI Chatbot
84
+ Combines the Wilderness Guide AI and the Wilderness First-Aid RAG manuals into a single chatbot interface tab. Features a side-by-side split screen with an Emergency Card and Quick Search Manual sidebar alongside the main chatbot window.
85
+
86
+ ### 4. πŸŽ™οΈ Geotagged Voice Journal & Storyteller
87
+ Hikers can log voice entries while hiking. The application transcribes the audio, stamps it with current GPS coordinates and altitude, and stores it in an SQLite database. Post-trek, the LLM compiles these logs, stats, and POI encounters into an engaging, non-technical, shareable social media expedition report.
88
 
89
  ---
90
 
requirements.txt CHANGED
@@ -9,10 +9,10 @@ plotly
9
  pydantic>=2.0.0,<2.11.0
10
  Pillow
11
  # ASR: transformers + torch as fallback for Hugging Face (no pywhispercpp available there)
12
- transformers
13
- torch
14
- soundfile
15
  # Local-only ASR dependencies (optional β€” only work when pywhispercpp native binaries are present)
16
- # pywhispercpp # Uncomment for local GPU-accelerated ASR
17
- # miniaudio # Uncomment for local audio resampling (required by pywhispercpp path)
18
- # llama-cpp-python # Uncomment to run local GGUF LLM instead of mock backend
 
9
  pydantic>=2.0.0,<2.11.0
10
  Pillow
11
  # ASR: transformers + torch as fallback for Hugging Face (no pywhispercpp available there)
12
+ # transformers
13
+ # torch
14
+ # soundfile
15
  # Local-only ASR dependencies (optional β€” only work when pywhispercpp native binaries are present)
16
+ pywhispercpp # Uncomment for local GPU-accelerated ASR
17
+ miniaudio # Uncomment for local audio resampling (required by pywhispercpp path)
18
+ llama-cpp-python # Uncomment to run local GGUF LLM instead of mock backend
src/llm.py CHANGED
@@ -14,7 +14,8 @@ try:
14
  default_backend = "llama_cpp"
15
  except ImportError:
16
  default_backend = "mock"
17
- BACKEND = os.environ.get("BACKEND", default_backend).lower()
 
18
 
19
  # Constants for Hugging Face Space model loading
20
  MODEL_REPO = "bartowski/google_gemma-4-E2B-it-GGUF"
@@ -334,39 +335,48 @@ def generate_mock(prompt, system="", image_path=None, audio_path=None, history=N
334
  else:
335
  response += "- No voice logs recorded.\n"
336
  else:
 
 
 
 
337
  response += "🌲 **MY WILDERNESS EXPEDITION REPORT** 🌲\n"
338
  response += "*Powered by Trailhead Tactical Trail Computer*\n\n"
339
- response += f"What an absolute journey! πŸ”οΈ Just finished an intense trek covering **{total_dist} km** with **{ele_gain} m** of vertical climb! Here is the live play-by-play of how it went down:\n\n"
 
340
 
341
- milestones = []
342
- for am in amenities:
343
- milestones.append(("amenity", am["km"], am))
344
- for log in voice_logs:
345
- milestones.append(("log", log["km"], log))
346
- milestones = sorted(milestones, key=lambda x: x[1])
 
 
 
 
 
 
347
 
348
- for m_type, km, data in milestones:
349
- if m_type == "amenity":
350
- response += f"πŸ“ **Km {km:.2f} | Amenity Spot** πŸŽ’\n"
351
- response += f"Encountered **{data['name']}** ({data['type']}) situated just {data['offset']}m off the path. A crucial waypoint for resource management!\n\n"
352
- elif m_type == "log":
353
- transcript_lower = data['transcript'].lower()
354
  icon = "πŸŽ™οΈ"
355
- title = "Hiker Log"
356
- if "water" in transcript_lower:
357
  icon = "πŸ’§"
358
  title = "Water Source & Hydration Check"
359
- elif "view" in transcript_lower or "point of view" in transcript_lower:
360
  icon = "πŸ‘οΈ"
361
  title = "Scenic Viewpoint Reflection"
362
  elif "finish" in transcript_lower or "complete" in transcript_lower:
363
  icon = "🏁"
364
  title = "Trek Completion Signoff"
365
-
366
- response += f"{icon} **Km {km:.2f} | {title}** πŸ“\n"
367
- response += f"Recorded voice entry at {data['alt']}m altitude:\n"
368
- response += f"> *\"{data['transcript']}\"*\n\n"
369
 
 
 
 
 
370
  response += "🏁 **Trek Complete!**\n"
371
  response += "Every step was worth it. Pushed my limits, managed my resources, and conquered the route. πŸ₯Ύ\n\n"
372
  response += "---\n"
@@ -455,8 +465,8 @@ def generate_llama_cpp(prompt, system="", image_path=None, audio_path=None, hist
455
  return
456
 
457
  init_duration = time.time() - start_time
458
- if init_duration > 35.0:
459
- print(f"[llm.py] Warning: Model loading took {init_duration:.2f}s (exceeded 35s limit). Disabling llama_cpp and falling back to mock backend.")
460
  generate_llama_cpp.disabled = True
461
  for chunk in generate_mock(prompt, system, image_path, audio_path, history):
462
  yield chunk
@@ -490,7 +500,7 @@ def generate_llama_cpp(prompt, system="", image_path=None, audio_path=None, hist
490
  stream=True
491
  )
492
 
493
- first_token_timeout = 30.0
494
  response_iter = iter(response)
495
 
496
  first_chunk_start = time.time()
 
14
  default_backend = "llama_cpp"
15
  except ImportError:
16
  default_backend = "mock"
17
+ BACKEND = "llama_cpp" # Force llama_cpp backend
18
+
19
 
20
  # Constants for Hugging Face Space model loading
21
  MODEL_REPO = "bartowski/google_gemma-4-E2B-it-GGUF"
 
335
  else:
336
  response += "- No voice logs recorded.\n"
337
  else:
338
+ water_count = sum(1 for am in amenities if "water" in am["name"].lower() or "fountain" in am["name"].lower())
339
+ camp_count = sum(1 for am in amenities if "camp" in am["name"].lower() or "shelter" in am["name"].lower())
340
+ other_count = len(amenities) - water_count - camp_count
341
+
342
  response += "🌲 **MY WILDERNESS EXPEDITION REPORT** 🌲\n"
343
  response += "*Powered by Trailhead Tactical Trail Computer*\n\n"
344
+ response += f"What an absolute journey! πŸ”οΈ Just finished an intense trek covering **{total_dist} km** with **{ele_gain} m** of vertical climb! "
345
+ response += f"The altitude range profile spanned from **{alt_range}**, offering challenging terrain but rewarding views.\n\n"
346
 
347
+ response += "### πŸ₯Ύ The Journey & Resource Milestones\n"
348
+ response += "Setting off, the trail presented a rugged path but was well-equipped for resource management. "
349
+ if water_count > 0 or camp_count > 0 or other_count > 0:
350
+ parts = []
351
+ if water_count > 0:
352
+ parts.append(f"{water_count} drinking water and fountain stations")
353
+ if camp_count > 0:
354
+ parts.append(f"{camp_count} campsite/shelter areas")
355
+ if other_count > 0:
356
+ parts.append(f"{other_count} other points of interest")
357
+ response += f"Along the way, I passed through **{', '.join(parts)}** situated conveniently off the path, ensuring hydration and safety were never compromised. "
358
+ response += "Navigating these waypoints required careful planning, but it paid off beautifully.\n\n"
359
 
360
+ if voice_logs:
361
+ response += "### πŸŽ™οΈ Trail Reflections & Audio Log Highlights\n"
362
+ for log in voice_logs:
363
+ transcript_lower = log['transcript'].lower()
 
 
364
  icon = "πŸŽ™οΈ"
365
+ title = "Trail Observation"
366
+ if "water" in transcript_lower or "waterfall" in transcript_lower:
367
  icon = "πŸ’§"
368
  title = "Water Source & Hydration Check"
369
+ elif "view" in transcript_lower or "scenic" in transcript_lower:
370
  icon = "πŸ‘οΈ"
371
  title = "Scenic Viewpoint Reflection"
372
  elif "finish" in transcript_lower or "complete" in transcript_lower:
373
  icon = "🏁"
374
  title = "Trek Completion Signoff"
 
 
 
 
375
 
376
+ response += f"{icon} **Km {log['km']:.2f} | {title}** πŸ“\n"
377
+ response += f"Recorded voice entry at {log['alt']}m altitude:\n"
378
+ response += f"> *\"{log['transcript']}\"*\n\n"
379
+
380
  response += "🏁 **Trek Complete!**\n"
381
  response += "Every step was worth it. Pushed my limits, managed my resources, and conquered the route. πŸ₯Ύ\n\n"
382
  response += "---\n"
 
465
  return
466
 
467
  init_duration = time.time() - start_time
468
+ if init_duration > 120.0:
469
+ print(f"[llm.py] Warning: Model loading took {init_duration:.2f}s (exceeded 120s limit). Disabling llama_cpp and falling back to mock backend.")
470
  generate_llama_cpp.disabled = True
471
  for chunk in generate_mock(prompt, system, image_path, audio_path, history):
472
  yield chunk
 
500
  stream=True
501
  )
502
 
503
+ first_token_timeout = 120.0
504
  response_iter = iter(response)
505
 
506
  first_chunk_start = time.time()