ddecosmo commited on
Commit
0e56c4d
Β·
verified Β·
1 Parent(s): f86ecec

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +183 -183
app.py CHANGED
@@ -1,6 +1,9 @@
1
  """
2
- Lanternfly Field Capture Space
3
  A Gradio app for capturing photos with GPS coordinates and saving to Hugging Face datasets.
 
 
 
4
  """
5
 
6
  import gradio as gr
@@ -11,62 +14,63 @@ from datetime import datetime
11
  from PIL import Image
12
  from huggingface_hub import HfApi, hf_hub_download, create_repo, file_exists, upload_file
13
  import io
 
 
14
 
15
  # Configuration
16
- HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HF_TOKEN_SPACE") # Try both token sources
17
- DATASET_REPO = os.getenv("DATASET_REPO", "rlogh/lanternfly-data") # Default to your dataset
 
18
 
19
  # Initialize HF API only if credentials are available
20
  api = None
21
  if HF_TOKEN and DATASET_REPO:
22
- api = HfApi(token=HF_TOKEN)
23
- # Ensure dataset repo exists (idempotent)
24
  try:
 
 
25
  create_repo(DATASET_REPO, repo_type="dataset", exist_ok=True, token=HF_TOKEN)
26
  print("βœ… Hugging Face credentials found - dataset saving enabled")
27
  except Exception as e:
28
- print(f"⚠️ Error creating dataset repo: {e}")
29
  api = None
30
  else:
31
- print("⚠️ Running in test mode - no HF credentials (dataset saving disabled)")
32
 
33
  # Constants for file paths
34
  METADATA_PATH = "metadata/entries.jsonl"
35
  IMAGES_DIR = "images"
36
 
 
 
37
  def get_current_time():
38
- """Get current timestamp"""
39
  return datetime.now().isoformat()
40
 
41
  def _append_jsonl_in_repo(new_row: dict) -> None:
42
- """
43
- Appends a JSON line to metadata/entries.jsonl in the dataset repo.
44
- Downloads the existing file (if any), appends, and uploads back.
45
- """
46
- # Create a temp file in memory to reupload
47
- # First, try to download existing entries.jsonl (if present)
48
  buf = io.BytesIO()
49
  existing_lines = []
50
- try:
51
- if file_exists(DATASET_REPO, METADATA_PATH, repo_type="dataset", token=HF_TOKEN):
52
- local_path = hf_hub_download(
53
- repo_id=DATASET_REPO, filename=METADATA_PATH,
54
- repo_type="dataset", token=HF_TOKEN
55
- )
56
- with open(local_path, "r", encoding="utf-8") as f:
57
- existing_lines = f.read().splitlines()
58
- except Exception:
59
- # If download fails for any reason, proceed as if file doesn't exist
60
- existing_lines = []
 
 
 
61
 
62
- # Append our new row
63
  existing_lines.append(json.dumps(new_row, ensure_ascii=False))
64
-
65
- # Write back to buffer
66
  data = "\n".join(existing_lines).encode("utf-8")
67
  buf.write(data); buf.seek(0)
68
 
69
- # Upload to the same path (commit creates or updates the file)
70
  upload_file(
71
  path_or_fileobj=buf,
72
  path_in_repo=METADATA_PATH,
@@ -77,9 +81,9 @@ def _append_jsonl_in_repo(new_row: dict) -> None:
77
  )
78
 
79
  def _save_image_to_repo(pil_img: Image.Image, dest_rel_path: str) -> None:
80
- """
81
- Uploads a PIL image into the dataset repo (e.g., images/<uuid>.jpg).
82
- """
83
  img_bytes = io.BytesIO()
84
  pil_img.save(img_bytes, format="JPEG", quality=90)
85
  img_bytes.seek(0)
@@ -93,242 +97,237 @@ def _save_image_to_repo(pil_img: Image.Image, dest_rel_path: str) -> None:
93
  commit_message=f"Upload image {dest_rel_path}",
94
  )
95
 
96
- def handle_time_capture():
97
- """Handle time capture and return status message"""
98
- timestamp = get_current_time()
99
- status_msg = f"πŸ• **Time Captured**: {timestamp}"
100
- return status_msg, timestamp
101
-
102
  def handle_gps_location(json_str):
103
- """Handle GPS location data from JavaScript and return values for the textboxes"""
 
 
 
 
 
 
 
104
  try:
105
  data = json.loads(json_str)
 
106
  if 'error' in data:
107
- status_msg = f"❌ **GPS Error**: {data['error']}"
108
- return status_msg, data['error'], "", "", ""
109
-
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  lat = str(data.get('latitude', ''))
111
  lon = str(data.get('longitude', ''))
112
  accuracy = str(data.get('accuracy', ''))
113
- timestamp = data.get('timestamp', '')
114
-
115
- # Convert timestamp to ISO string if it's a number
116
- if timestamp and isinstance(timestamp, (int, float)):
117
- from datetime import datetime
118
- timestamp = datetime.fromtimestamp(timestamp / 1000).isoformat()
119
 
120
- status_msg = f"βœ… **GPS Captured**: {lat[:8]}, {lon[:8]} (accuracy: {accuracy}m)"
 
 
 
 
 
121
  return status_msg, lat, lon, accuracy, timestamp
122
-
123
  except Exception as e:
124
- status_msg = f"❌ **Error**: {str(e)}"
125
- return status_msg, f"Error parsing GPS data: {str(e)}", "", "", ""
 
 
 
126
 
127
  def get_gps_js():
128
- """JavaScript for GPS capture using hidden textbox approach"""
129
  return """
130
  () => {
131
- // find the textarea element inside Gradio textbox by its elem_id
132
- const textarea = document.querySelector('#hidden_gps_input textarea');
133
- if (!textarea) {
134
- console.log("Hidden GPS textbox not found");
135
- return;
136
- }
137
- if (!navigator.geolocation) {
138
- textarea.value = JSON.stringify({error: "Geolocation not supported"});
139
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
140
- return;
141
- }
142
- navigator.geolocation.getCurrentPosition(
143
- function(position) {
144
- const data = {
145
- latitude: position.coords.latitude,
146
- longitude: position.coords.longitude,
147
- accuracy: position.coords.accuracy,
148
- timestamp: new Date().toISOString()
149
- };
150
- textarea.value = JSON.stringify(data);
151
- // dispatch 'input' event so Gradio notices the change
152
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
153
- },
154
- function(err) {
155
- textarea.value = JSON.stringify({ error: err.message });
156
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
157
- },
158
- { enableHighAccuracy: true, timeout: 10000 }
159
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  }
161
  """
162
 
163
  def save_to_dataset(image, lat, lon, accuracy_m, device_ts):
164
- """
165
- Save image and metadata to Hugging Face dataset
166
-
167
- Args:
168
- image: PIL Image object
169
- lat: latitude as string
170
- lon: longitude as string
171
- accuracy_m: accuracy in meters as string
172
- device_ts: device timestamp as string
173
-
174
- Returns:
175
- tuple: (status_markdown, preview_json)
176
- """
177
  try:
178
- # Validate inputs
179
  if image is None:
180
  return "❌ **Error**: No image captured. Please take a photo first.", ""
181
-
182
- # πŸ”’ Ensure PIL.Image
183
- try:
184
- from PIL import Image as _PILImage
185
- import numpy as _np
186
- if isinstance(image, _np.ndarray):
187
- image = _PILImage.fromarray(image)
188
- except Exception:
189
- pass
190
-
191
- if not lat or not lon:
192
  return "❌ **Error**: GPS coordinates missing. Please click 'Get GPS' first.", ""
193
-
194
- # Check if running in test mode
195
  if not api:
196
- # Test mode - just preview the data without saving
197
  server_ts = datetime.now().isoformat()
198
  img_id = str(uuid.uuid4())
199
-
200
  timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
201
  row = {
202
- "id": img_id,
203
- "image": f"lanternfly_{timestamp_str}_{img_id[:8]}.jpg",
204
- "latitude": float(lat) if lat else None,
205
- "longitude": float(lon) if lon else None,
206
- "accuracy_m": float(accuracy_m) if accuracy_m else None,
207
- "device_timestamp": device_ts if device_ts else None,
208
- "server_timestamp_utc": server_ts,
209
- "notes": ""
210
  }
211
-
212
  status = f"πŸ” **Test Mode**: Data validated successfully! Sample {img_id[:8]}"
213
  preview = json.dumps(row, indent=2)
214
  return status, preview
215
-
216
- # Build a unique ID and paths with timestamp for better visibility
217
  sample_id = str(uuid.uuid4())
218
  timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
219
- image_rel_path = f"lanternfly_{timestamp_str}_{sample_id[:8]}.jpg"
220
-
221
- # Save image first
222
- try:
223
- _save_image_to_repo(image, image_rel_path)
224
- except Exception as e:
225
- return f"❌ **Error**: Failed to upload image: {e}", ""
226
-
227
- # Server UTC timestamp
228
  server_ts_utc = datetime.now().isoformat() + "Z"
229
-
230
- # Construct metadata row
231
  row = {
232
- "id": sample_id,
233
- "image": image_rel_path, # This will be visible in dataset viewer
234
- "latitude": float(lat) if lat else None,
235
- "longitude": float(lon) if lon else None,
236
- "accuracy_m": float(accuracy_m) if accuracy_m else None,
237
- "device_timestamp": device_ts if device_ts else None,
238
  "server_timestamp_utc": server_ts_utc,
239
- "location": f"{lat}, {lon}" if lat and lon else None, # Human-readable location
240
- "notes": "" # placeholder for future labels
241
  }
242
-
243
- # Append metadata row
244
- try:
245
- _append_jsonl_in_repo(row)
246
- except Exception as e:
247
- return f"❌ **Error**: Image uploaded, but failed to append metadata: {e}", ""
248
-
249
- # Return success message and preview
250
  status = (
251
  "βœ… **Success!** Saved to dataset!\n\n"
252
  f"- Image: `{image_rel_path}`\n"
253
- f"- Lat/Lon: {row['latitude']}, {row['longitude']} (Β±{row['accuracy_m']} m)\n"
254
- f"- Server time (UTC): {server_ts_utc}"
255
  )
256
  preview = json.dumps(row, indent=2)
257
-
258
  return status, preview
259
-
260
  except Exception as e:
261
- error_msg = f"❌ **Error**: {str(e)}"
 
262
  return error_msg, ""
263
 
264
- # Create Gradio interface
 
265
  with gr.Blocks(title="Lanternfly Field Capture") as app:
266
- gr.Markdown("# πŸ¦‹ Lanternfly Field Capture")
267
- gr.Markdown("Capture photos with GPS coordinates for field research data collection.")
268
 
 
 
 
269
  with gr.Row():
270
  with gr.Column(scale=1):
271
  # Camera input
272
  camera = gr.Image(
273
  streaming=False,
274
  height=380,
275
- label="πŸ“· Upload Photo (or use camera)",
276
  type="pil",
277
  sources=["webcam", "upload"]
278
  )
279
 
280
- # GPS data capture
281
- gr.Markdown("### πŸ“ GPS Coordinates")
282
- gr.Markdown("Click the button below to automatically capture your location and timestamp.")
283
 
284
  # GPS capture button
285
  gps_btn = gr.Button("πŸ“ Get GPS", variant="primary")
286
 
287
- # Hidden input for GPS data
288
- hidden_gps_input = gr.Textbox(visible=False, elem_id="hidden_gps_input")
289
-
290
- with gr.Row():
291
- lat_box = gr.Textbox(label="Latitude", interactive=True, elem_id="lat")
292
- lon_box = gr.Textbox(label="Longitude", interactive=True, elem_id="lon")
293
-
294
- with gr.Row():
295
- accuracy_box = gr.Textbox(label="Accuracy (meters)", interactive=True, elem_id="accuracy")
296
- device_ts_box = gr.Textbox(label="Device Timestamp", interactive=True, elem_id="device_ts")
297
-
298
  # Time capture button
299
  time_btn = gr.Button("πŸ• Get Current Time", variant="secondary")
300
 
301
  # Save button
302
- save_btn = gr.Button("πŸ’Ύ Save to Dataset", variant="secondary")
303
-
304
  with gr.Column(scale=1):
305
  # Status display
306
- status = gr.Markdown("πŸ”„ **Ready to capture data...** Click 'Get GPS' to start or upload a photo.")
 
 
 
 
 
 
 
 
 
307
 
308
  # Preview JSON
309
- preview = gr.JSON(label="Preview JSON", visible=True)
310
-
311
- # Event handlers
 
 
312
  gps_btn.click(
313
  fn=None,
314
  inputs=[],
315
  outputs=[],
316
  js=get_gps_js()
317
  )
318
-
319
- # When the hidden GPS input changes, populate the visible fields
320
  hidden_gps_input.change(
321
  fn=handle_gps_location,
322
  inputs=[hidden_gps_input],
323
  outputs=[status, lat_box, lon_box, accuracy_box, device_ts_box]
324
  )
325
-
 
326
  time_btn.click(
327
  fn=handle_time_capture,
328
  inputs=[],
329
  outputs=[status, device_ts_box]
330
  )
331
-
 
332
  save_btn.click(
333
  fn=save_to_dataset,
334
  inputs=[camera, lat_box, lon_box, accuracy_box, device_ts_box],
@@ -337,4 +336,5 @@ with gr.Blocks(title="Lanternfly Field Capture") as app:
337
 
338
  # Launch the app
339
  if __name__ == "__main__":
340
- app.launch()
 
 
1
  """
2
+ Lanternfly Field Capture Space - Resilient GPS (V9)
3
  A Gradio app for capturing photos with GPS coordinates and saving to Hugging Face datasets.
4
+
5
+ This version incorporates all debugging fixes: safe handling of empty input,
6
+ resilient component selection, and relaxed GPS timeout settings.
7
  """
8
 
9
  import gradio as gr
 
14
  from PIL import Image
15
  from huggingface_hub import HfApi, hf_hub_download, create_repo, file_exists, upload_file
16
  import io
17
+ import time
18
+ import requests
19
 
20
  # Configuration
21
+ # NOTE: Set HF_TOKEN environment variable in Colab or your Hugging Face Space settings.
22
+ HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HF_TOKEN_SPACE")
23
+ DATASET_REPO = os.getenv("DATASET_REPO", "rlogh/lanternfly-data")
24
 
25
  # Initialize HF API only if credentials are available
26
  api = None
27
  if HF_TOKEN and DATASET_REPO:
 
 
28
  try:
29
+ # Initializing HfApi inside the running app environment
30
+ api = HfApi(token=HF_TOKEN)
31
  create_repo(DATASET_REPO, repo_type="dataset", exist_ok=True, token=HF_TOKEN)
32
  print("βœ… Hugging Face credentials found - dataset saving enabled")
33
  except Exception as e:
34
+ print(f"⚠️ Error initializing HF API: {e}")
35
  api = None
36
  else:
37
+ print("⚠️ Running in test mode - no HF credentials (dataset saving disabled)")
38
 
39
  # Constants for file paths
40
  METADATA_PATH = "metadata/entries.jsonl"
41
  IMAGES_DIR = "images"
42
 
43
+ # --- Utility Functions ---
44
+
45
  def get_current_time():
46
+ """Get current timestamp in ISO format"""
47
  return datetime.now().isoformat()
48
 
49
  def _append_jsonl_in_repo(new_row: dict) -> None:
50
+ """Appends a JSON line to metadata/entries.jsonl in the dataset repo."""
51
+ if not api: return
52
+
 
 
 
53
  buf = io.BytesIO()
54
  existing_lines = []
55
+
56
+ for i in range(3):
57
+ try:
58
+ if file_exists(DATASET_REPO, METADATA_PATH, repo_type="dataset", token=HF_TOKEN):
59
+ local_path = hf_hub_download(
60
+ repo_id=DATASET_REPO, filename=METADATA_PATH,
61
+ repo_type="dataset", token=HF_TOKEN
62
+ )
63
+ with open(local_path, "r", encoding="utf-8") as f:
64
+ existing_lines = f.read().splitlines()
65
+ break
66
+ except Exception as e:
67
+ if i == 2: raise e
68
+ time.sleep(1 * (i + 1))
69
 
 
70
  existing_lines.append(json.dumps(new_row, ensure_ascii=False))
 
 
71
  data = "\n".join(existing_lines).encode("utf-8")
72
  buf.write(data); buf.seek(0)
73
 
 
74
  upload_file(
75
  path_or_fileobj=buf,
76
  path_in_repo=METADATA_PATH,
 
81
  )
82
 
83
  def _save_image_to_repo(pil_img: Image.Image, dest_rel_path: str) -> None:
84
+ """Uploads a PIL image into the dataset repo."""
85
+ if not api: return
86
+
87
  img_bytes = io.BytesIO()
88
  pil_img.save(img_bytes, format="JPEG", quality=90)
89
  img_bytes.seek(0)
 
97
  commit_message=f"Upload image {dest_rel_path}",
98
  )
99
 
 
 
 
 
 
 
100
  def handle_gps_location(json_str):
101
+ """
102
+ Handles GPS location data from JavaScript.
103
+ Includes V8 fix to prevent crash on empty initial input.
104
+ """
105
+ # V8 FIX: Ignore empty input from initial component change event
106
+ if not json_str:
107
+ return gr.NoAction(), gr.NoAction(), gr.NoAction(), gr.NoAction(), gr.NoAction()
108
+
109
  try:
110
  data = json.loads(json_str)
111
+
112
  if 'error' in data:
113
+ # Map Geolocation API error codes for better user feedback
114
+ error_map = {
115
+ 1: "Permission Denied (Check browser settings)",
116
+ 2: "Position Unavailable (Poor signal/network)",
117
+ 3: "Timeout Expired (Fix took too long)",
118
+ 0: "Geolocation not supported",
119
+ 'N/A': "Unknown Geolocation Error"
120
+ }
121
+ error_code = data.get('code', 'N/A')
122
+ error_msg = error_map.get(error_code, data.get('error', 'Unknown Error'))
123
+
124
+ gr.Warning(f"GPS Error: Code {error_code} ({error_msg})")
125
+
126
+ status_msg = f"❌ **GPS Error (Code {error_code})**: {error_msg}"
127
+ return status_msg, "N/A", "N/A", "N/A", get_current_time()
128
+
129
  lat = str(data.get('latitude', ''))
130
  lon = str(data.get('longitude', ''))
131
  accuracy = str(data.get('accuracy', ''))
132
+ timestamp = str(data.get('timestamp', ''))
 
 
 
 
 
133
 
134
+ try:
135
+ acc_display = f"{float(accuracy):.1f}"
136
+ except ValueError:
137
+ acc_display = "N/A"
138
+
139
+ status_msg = f"βœ… **GPS Captured**: {lat[:8]}, {lon[:8]} (accuracy: {acc_display}m)"
140
  return status_msg, lat, lon, accuracy, timestamp
141
+
142
  except Exception as e:
143
+ # Catch unexpected JSON errors (shouldn't happen with V8 fix)
144
+ status_msg = f"❌ **Error**: Failed to process GPS JSON: {str(e)}"
145
+ gr.Error(status_msg)
146
+ return status_msg, "Error", "Error", "Error", "Error"
147
+
148
 
149
  def get_gps_js():
150
+ """JavaScript for robust, manually-triggered GPS capture."""
151
  return """
152
  () => {
153
+ // Find the specific hidden textarea within the component container
154
+ const container = document.querySelector('#hidden_gps_input');
155
+ let textarea = null;
156
+
157
+ if (container) {
158
+ // Find the actual textarea element inside the Gradio wrapper
159
+ textarea = container.querySelector('textarea');
160
+ }
161
+
162
+ if (!textarea) {
163
+ console.error("DEBUG: Fatal: Hidden GPS textbox cannot be found.");
164
+ return;
165
+ }
166
+
167
+ if (!navigator.geolocation) {
168
+ console.error("DEBUG: Geolocation not supported by browser.");
169
+ textarea.value = JSON.stringify({error: "Geolocation not supported", code: 0});
170
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
171
+ return;
172
+ }
173
+
174
+ console.log("DEBUG: Starting Geolocation request (60s timeout, low accuracy preferred).");
175
+
176
+ navigator.geolocation.getCurrentPosition(
177
+ function(position) {
178
+ console.log("DEBUG: Geolocation SUCCESS.", position.coords);
179
+ const data = {
180
+ latitude: position.coords.latitude,
181
+ longitude: position.coords.longitude,
182
+ accuracy: position.coords.accuracy,
183
+ timestamp: new Date(position.timestamp).toISOString()
184
+ };
185
+
186
+ textarea.value = JSON.stringify(data);
187
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
188
+ },
189
+ function(err) {
190
+ // Pass the error code back to Python
191
+ console.error(`DEBUG: Geolocation FAILURE. Code: ${err.code}, Message: ${err.message}`);
192
+ textarea.value = JSON.stringify({ error: err.message, code: err.code });
193
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
194
+ },
195
+ // Options: enableHighAccuracy: false for faster fix, maximumAge for caching, 60s timeout
196
+ { enableHighAccuracy: false, timeout: 60000, maximumAge: 5000 }
197
+ );
198
  }
199
  """
200
 
201
  def save_to_dataset(image, lat, lon, accuracy_m, device_ts):
202
+ """Save image and metadata to Hugging Face dataset"""
 
 
 
 
 
 
 
 
 
 
 
 
203
  try:
 
204
  if image is None:
205
  return "❌ **Error**: No image captured. Please take a photo first.", ""
206
+ if lat == "N/A" or lon == "N/A":
 
 
 
 
 
 
 
 
 
 
207
  return "❌ **Error**: GPS coordinates missing. Please click 'Get GPS' first.", ""
208
+
209
+ # Test Mode Check (If API is not initialized)
210
  if not api:
 
211
  server_ts = datetime.now().isoformat()
212
  img_id = str(uuid.uuid4())
 
213
  timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
214
  row = {
215
+ "id": img_id, "image": f"test_{timestamp_str}_{img_id[:8]}.jpg",
216
+ "latitude": float(lat) if lat != 'N/A' else None, "longitude": float(lon) if lon != 'N/A' else None,
217
+ "accuracy_m": accuracy_m, "device_timestamp": device_ts,
218
+ "server_timestamp_utc": server_ts, "notes": ""
 
 
 
 
219
  }
 
220
  status = f"πŸ” **Test Mode**: Data validated successfully! Sample {img_id[:8]}"
221
  preview = json.dumps(row, indent=2)
222
  return status, preview
223
+
224
+ # Normal Save Process
225
  sample_id = str(uuid.uuid4())
226
  timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
227
+ image_rel_path = f"{IMAGES_DIR}/lanternfly_{timestamp_str}_{sample_id[:8]}.jpg"
228
+
229
+ _save_image_to_repo(image, image_rel_path)
230
+
 
 
 
 
 
231
  server_ts_utc = datetime.now().isoformat() + "Z"
232
+
 
233
  row = {
234
+ "id": sample_id, "image": image_rel_path,
235
+ "latitude": float(lat), "longitude": float(lon),
236
+ "accuracy_m": float(accuracy_m),
237
+ "device_timestamp": device_ts,
 
 
238
  "server_timestamp_utc": server_ts_utc,
239
+ "location": f"{lat}, {lon}",
240
+ "notes": ""
241
  }
242
+
243
+ _append_jsonl_in_repo(row)
244
+
 
 
 
 
 
245
  status = (
246
  "βœ… **Success!** Saved to dataset!\n\n"
247
  f"- Image: `{image_rel_path}`\n"
248
+ f"- Lat/Lon: {row['latitude']}, {row['longitude']} (Β±{row['accuracy_m']} m)"
 
249
  )
250
  preview = json.dumps(row, indent=2)
 
251
  return status, preview
252
+
253
  except Exception as e:
254
+ error_msg = f"❌ **Critical Save Error**: {str(e)}"
255
+ gr.Error(error_msg)
256
  return error_msg, ""
257
 
258
+ # --- Gradio Interface ---
259
+
260
  with gr.Blocks(title="Lanternfly Field Capture") as app:
261
+ gr.Markdown("# πŸ¦‹ Lanternfly Field Capture (Resilient GPS)")
262
+ gr.Markdown("Click **'πŸ“ Get GPS'** to capture location. **You must allow location permission** in your browser.")
263
 
264
+ # Hidden input for GPS data - MUST retain this ID for the JavaScript selector to work
265
+ hidden_gps_input = gr.Textbox(visible=False, elem_id="hidden_gps_input")
266
+
267
  with gr.Row():
268
  with gr.Column(scale=1):
269
  # Camera input
270
  camera = gr.Image(
271
  streaming=False,
272
  height=380,
273
+ label="πŸ“· Capture or Upload Photo",
274
  type="pil",
275
  sources=["webcam", "upload"]
276
  )
277
 
278
+ # --- GPS Capture Section ---
279
+ gr.Markdown("### πŸ“ Location Capture")
 
280
 
281
  # GPS capture button
282
  gps_btn = gr.Button("πŸ“ Get GPS", variant="primary")
283
 
 
 
 
 
 
 
 
 
 
 
 
284
  # Time capture button
285
  time_btn = gr.Button("πŸ• Get Current Time", variant="secondary")
286
 
287
  # Save button
288
+ save_btn = gr.Button("πŸ’Ύ Save Photo and Data to Dataset", variant="stop")
289
+
290
  with gr.Column(scale=1):
291
  # Status display
292
+ status = gr.Markdown("πŸ”„ **Ready to capture data...**")
293
+
294
+ # Location Data Fields
295
+ gr.Markdown("### Captured Data")
296
+ with gr.Row():
297
+ lat_box = gr.Textbox(label="Latitude", interactive=True, value="N/A")
298
+ lon_box = gr.Textbox(label="Longitude", interactive=True, value="N/A")
299
+ with gr.Row():
300
+ accuracy_box = gr.Textbox(label="Accuracy (meters)", interactive=True, value="N/A")
301
+ device_ts_box = gr.Textbox(label="Device Timestamp", interactive=True, value="N/A")
302
 
303
  # Preview JSON
304
+ preview = gr.JSON(label="Preview Data Payload", visible=True)
305
+
306
+ # --- Event Handlers ---
307
+
308
+ # 1. GPS Button Click triggers JavaScript injection to run Geolocation API
309
  gps_btn.click(
310
  fn=None,
311
  inputs=[],
312
  outputs=[],
313
  js=get_gps_js()
314
  )
315
+
316
+ # 2. Hidden GPS input change triggers Python backend processing (safe from empty string crash)
317
  hidden_gps_input.change(
318
  fn=handle_gps_location,
319
  inputs=[hidden_gps_input],
320
  outputs=[status, lat_box, lon_box, accuracy_box, device_ts_box]
321
  )
322
+
323
+ # 3. Time Button Click
324
  time_btn.click(
325
  fn=handle_time_capture,
326
  inputs=[],
327
  outputs=[status, device_ts_box]
328
  )
329
+
330
+ # 4. Save button click
331
  save_btn.click(
332
  fn=save_to_dataset,
333
  inputs=[camera, lat_box, lon_box, accuracy_box, device_ts_box],
 
336
 
337
  # Launch the app
338
  if __name__ == "__main__":
339
+ # In Colab, share=True is mandatory for HTTPS
340
+ app.launch(share=True)