RisingZhang commited on
Commit
81b6726
·
0 Parent(s):

first commit

Browse files
Files changed (5) hide show
  1. .gitattributes +2 -0
  2. .gitignore +4 -0
  3. app.py +618 -0
  4. data/dataset.csv +0 -0
  5. player_runs.csv +14 -0
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
2
+ *.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ game_app2.py
4
+ game_app3.py
app.py ADDED
@@ -0,0 +1,618 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import datetime as dt
5
+ import math
6
+ import random
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional, Tuple
10
+
11
+ import numpy as np
12
+
13
+ if not hasattr(np, "bool"): # pandas<2 compatibility on numpy>=2
14
+ np.bool = bool # type: ignore[attr-defined]
15
+
16
+ import pandas as pd
17
+ from PIL import Image, ImageDraw
18
+
19
+ import gradio as gr
20
+
21
+
22
+ BASE_DIR = Path(__file__).resolve().parent
23
+ DATASET_PATH = BASE_DIR / "data" / "dataset.csv"
24
+ AUDIO_BASE_DIR = BASE_DIR / "data" / "audios"
25
+ ASSETS_DIR = BASE_DIR / "assets"
26
+ MAP_IMAGE_PATH = ASSETS_DIR / "world_map.png"
27
+ LOG_PATH = BASE_DIR / "player_runs.csv"
28
+ RECENT_COLUMNS = ["timestamp", "player_id", "question_id", "distance_km"]
29
+
30
+
31
+ @dataclass
32
+ class Sample:
33
+ question_id: str
34
+ audio_path: Path
35
+ longitude: float
36
+ latitude: float
37
+ city: str
38
+ country: str
39
+ continent: str
40
+ description: str
41
+ title: str
42
+
43
+
44
+ def _load_samples() -> List[Sample]:
45
+ if not DATASET_PATH.exists():
46
+ raise FileNotFoundError(f"Dataset not found at {DATASET_PATH}")
47
+
48
+ df = pd.read_csv(DATASET_PATH)
49
+ start_idx = int(len(df) * 0.9)
50
+ test_df = df.iloc[start_idx:].reset_index(drop=True)
51
+
52
+ samples: List[Sample] = []
53
+ missing_audio = 0
54
+ missing_coords = 0
55
+
56
+ for row in test_df.itertuples():
57
+ audio_path = AUDIO_BASE_DIR / getattr(row, "mp3name")
58
+ longitude = getattr(row, "longitude")
59
+ latitude = getattr(row, "latitude")
60
+
61
+ if math.isnan(longitude) or math.isnan(latitude):
62
+ missing_coords += 1
63
+ continue
64
+
65
+ if not audio_path.exists():
66
+ missing_audio += 1
67
+ continue
68
+
69
+ samples.append(
70
+ Sample(
71
+ question_id=str(getattr(row, "key")),
72
+ audio_path=audio_path,
73
+ longitude=float(longitude),
74
+ latitude=float(latitude),
75
+ city=str(getattr(row, "city", "") or ""),
76
+ country=str(getattr(row, "country", "") or ""),
77
+ continent=str(getattr(row, "continent", "") or ""),
78
+ description=str(getattr(row, "description", "") or ""),
79
+ title=str(getattr(row, "title", "") or ""),
80
+ )
81
+ )
82
+
83
+ if not samples:
84
+ raise RuntimeError("No playable samples were found in the test split.")
85
+
86
+ if missing_audio:
87
+ print(f"[game_app] Skipped {missing_audio} samples because audio files are missing.")
88
+ if missing_coords:
89
+ print(f"[game_app] Skipped {missing_coords} samples because coordinates are missing.")
90
+
91
+ return samples
92
+
93
+
94
+ SAMPLES: List[Sample] = _load_samples()
95
+ BASE_MAP_IMAGE = Image.open(MAP_IMAGE_PATH).convert("RGB")
96
+ MAP_WIDTH, MAP_HEIGHT = BASE_MAP_IMAGE.size
97
+
98
+
99
+ def _random_queue() -> List[int]:
100
+ queue = list(range(len(SAMPLES)))
101
+ random.shuffle(queue)
102
+ return queue
103
+
104
+
105
+ def _pixel_to_latlon(x: int, y: int) -> Tuple[float, float]:
106
+ lon = (x / (MAP_WIDTH - 1)) * 360.0 - 180.0
107
+ lat = 90.0 - (y / (MAP_HEIGHT - 1)) * 180.0
108
+ return round(lat, 6), round(lon, 6)
109
+
110
+
111
+ def _latlon_to_text(lat: float, lon: float) -> str:
112
+ return f"Selected latitude {lat:.3f}°, longitude {lon:.3f}°"
113
+
114
+
115
+ def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
116
+ r = 6371.0
117
+ phi1, phi2 = math.radians(lat1), math.radians(lat2)
118
+ d_phi = math.radians(lat2 - lat1)
119
+ d_lambda = math.radians(lon2 - lon1)
120
+
121
+ a = math.sin(d_phi / 2.0) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(d_lambda / 2.0) ** 2
122
+ c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
123
+ return r * c
124
+
125
+
126
+ def _map_with_marker(x: int, y: int) -> np.ndarray:
127
+ marker_radius = max(6, MAP_WIDTH // 150)
128
+ img = BASE_MAP_IMAGE.copy()
129
+ draw = ImageDraw.Draw(img)
130
+ draw.ellipse(
131
+ (
132
+ x - marker_radius,
133
+ y - marker_radius,
134
+ x + marker_radius,
135
+ y + marker_radius,
136
+ ),
137
+ fill=(225, 64, 64),
138
+ outline=(0, 0, 0),
139
+ width=2,
140
+ )
141
+ return np.array(img)
142
+
143
+
144
+ def _base_map_array() -> np.ndarray:
145
+ return np.array(BASE_MAP_IMAGE)
146
+
147
+
148
+ def _prepare_clip_info(sample: Sample, round_idx: int) -> str:
149
+ intro_lines = [
150
+ f"**Round:** {round_idx}"
151
+ ]
152
+ return "\n\n".join(intro_lines)
153
+
154
+
155
+ def _append_log(entry: Dict[str, object]) -> None:
156
+ write_header = not LOG_PATH.exists()
157
+ with LOG_PATH.open("a", newline="", encoding="utf-8") as f:
158
+ writer = csv.DictWriter(f, fieldnames=list(entry.keys()))
159
+ if write_header:
160
+ writer.writeheader()
161
+ writer.writerow(entry)
162
+
163
+
164
+ def _load_recent_runs(limit: int = 5) -> List[Dict[str, object]]:
165
+ if not LOG_PATH.exists():
166
+ return []
167
+
168
+ rows = []
169
+ with LOG_PATH.open("r", encoding="utf-8") as f:
170
+ reader = csv.DictReader(f)
171
+ for row in reader:
172
+ rows.append(row)
173
+ return rows[-limit:]
174
+
175
+
176
+ def _format_recent_rows(rows: List[Dict[str, object]]) -> List[List[object]]:
177
+ formatted: List[List[object]] = []
178
+ for row in rows:
179
+ timestamp_str = row.get("timestamp", "")
180
+ if timestamp_str:
181
+ try:
182
+ # Parse ISO format timestamp and format as readable string
183
+ dt_obj = dt.datetime.fromisoformat(str(timestamp_str).replace('Z', '+00:00'))
184
+ formatted_timestamp = dt_obj.strftime("%Y-%m-%d %H:%M:%S")
185
+ except (ValueError, AttributeError):
186
+ formatted_timestamp = str(timestamp_str)
187
+ else:
188
+ formatted_timestamp = ""
189
+
190
+ formatted_row = [formatted_timestamp]
191
+ formatted_row.extend([row.get(col, "") for col in RECENT_COLUMNS[1:]])
192
+ formatted.append(formatted_row)
193
+ return formatted
194
+
195
+
196
+ def initialize_round() -> Tuple[Dict[str, object], Dict[str, Optional[float]], str, str, np.ndarray, str, List[List[object]], str, str]:
197
+ queue = _random_queue()
198
+ current_index = queue.pop()
199
+
200
+ state = {
201
+ "queue": queue,
202
+ "current_index": current_index,
203
+ "round": 1,
204
+ }
205
+
206
+ current_sample = SAMPLES[current_index]
207
+ audio_value = str(current_sample.audio_path)
208
+ clip_md = _prepare_clip_info(current_sample, state["round"])
209
+ prompt_text = "Click once on the map to pick your guess. The marker will update to your last selection."
210
+ guess_state = {"lat": None, "lon": None, "pixel": None}
211
+ recent_runs = _format_recent_rows(_load_recent_runs())
212
+
213
+ return state, guess_state, audio_value, clip_md, _base_map_array(), prompt_text, recent_runs, "", ""
214
+
215
+
216
+ def _next_sample(state: Dict[str, object]) -> Sample:
217
+ queue: List[int] = state["queue"]
218
+ if not queue:
219
+ queue.extend(_random_queue())
220
+
221
+ next_index = queue.pop()
222
+ state["current_index"] = next_index
223
+ state["round"] = state.get("round", 1) + 1
224
+ return SAMPLES[next_index]
225
+
226
+
227
+ def _ensure_guess_state(state: Optional[Dict[str, Optional[float]]]) -> Dict[str, Optional[float]]:
228
+ if not isinstance(state, dict):
229
+ return {"lat": None, "lon": None, "pixel": None}
230
+ return {
231
+ "lat": state.get("lat"),
232
+ "lon": state.get("lon"),
233
+ "pixel": state.get("pixel"),
234
+ }
235
+
236
+
237
+ def handle_map_click(
238
+ evt: gr.SelectData,
239
+ current_guess_state: Optional[Dict[str, Optional[float]]],
240
+ ) -> Tuple[np.ndarray, str, Dict[str, Optional[float]], str, str]:
241
+ guess_state = _ensure_guess_state(current_guess_state)
242
+ if evt is None:
243
+ return _base_map_array(), "Unable to read selection. Please try again.", guess_state, "", ""
244
+
245
+ index = getattr(evt, "index", None)
246
+ value = getattr(evt, "value", None)
247
+
248
+ x = y = None
249
+ if isinstance(index, (tuple, list)) and len(index) >= 2:
250
+ x, y = index[0], index[1]
251
+ elif isinstance(index, dict):
252
+ x = index.get("x")
253
+ y = index.get("y")
254
+
255
+ if (x is None or y is None) and isinstance(value, dict):
256
+ x = value.get("x", x)
257
+ y = value.get("y", y)
258
+ elif (x is None or y is None) and isinstance(value, (tuple, list)) and len(value) >= 2:
259
+ if x is None:
260
+ x = value[0]
261
+ if y is None:
262
+ y = value[1]
263
+
264
+ if x is None or y is None:
265
+ return _base_map_array(), "Unable to read selection. Please try again.", guess_state, "", ""
266
+
267
+ x = int(x)
268
+ y = int(y)
269
+
270
+ lat, lon = _pixel_to_latlon(x, y)
271
+ guess_state = {"lat": lat, "lon": lon, "pixel": (x, y)}
272
+ image_with_marker = _map_with_marker(x, y)
273
+ return image_with_marker, _latlon_to_text(lat, lon), guess_state, f"{lon:.6f}", f"{lat:.6f}"
274
+
275
+
276
+ def submit_guess(
277
+ player_id: str,
278
+ game_state: Dict[str, object],
279
+ guess_state: Optional[Dict[str, Optional[float]]],
280
+ longitude: str,
281
+ latitude: str,
282
+ ) -> Tuple[
283
+ Dict[str, object],
284
+ Dict[str, Optional[float]],
285
+ str,
286
+ str,
287
+ np.ndarray,
288
+ str,
289
+ List[List[object]],
290
+ str,
291
+ str,
292
+ ]:
293
+ guess_state = _ensure_guess_state(guess_state)
294
+ player_id = (player_id or "").strip()
295
+ if not player_id:
296
+ message = "Please enter your player ID before submitting."
297
+ current_sample = SAMPLES[game_state["current_index"]]
298
+ clip_md = _prepare_clip_info(current_sample, game_state.get("round", 1))
299
+ prompt_text = message
300
+ return (
301
+ game_state,
302
+ guess_state,
303
+ str(current_sample.audio_path),
304
+ clip_md,
305
+ _base_map_array(),
306
+ prompt_text,
307
+ _format_recent_rows(_load_recent_runs()),
308
+ longitude,
309
+ latitude,
310
+ )
311
+
312
+ # Parse longitude and latitude from text inputs
313
+ try:
314
+ longitude = longitude.strip() if longitude else ""
315
+ latitude = latitude.strip() if latitude else ""
316
+
317
+ if not longitude or not latitude:
318
+ message = "Please enter both longitude and latitude, or click on the map to select a location."
319
+ current_sample = SAMPLES[game_state["current_index"]]
320
+ clip_md = _prepare_clip_info(current_sample, game_state.get("round", 1))
321
+ prompt_text = message
322
+ return (
323
+ game_state,
324
+ guess_state,
325
+ str(current_sample.audio_path),
326
+ clip_md,
327
+ _base_map_array(),
328
+ prompt_text,
329
+ _format_recent_rows(_load_recent_runs()),
330
+ longitude,
331
+ latitude,
332
+ )
333
+
334
+ guess_lon = float(longitude)
335
+ guess_lat = float(latitude)
336
+
337
+ # Validate ranges
338
+ if not (-180 <= guess_lon <= 180):
339
+ message = "Longitude must be between -180 and 180."
340
+ current_sample = SAMPLES[game_state["current_index"]]
341
+ clip_md = _prepare_clip_info(current_sample, game_state.get("round", 1))
342
+ prompt_text = message
343
+ return (
344
+ game_state,
345
+ guess_state,
346
+ str(current_sample.audio_path),
347
+ clip_md,
348
+ _base_map_array(),
349
+ prompt_text,
350
+ _format_recent_rows(_load_recent_runs()),
351
+ longitude,
352
+ latitude,
353
+ )
354
+
355
+ if not (-90 <= guess_lat <= 90):
356
+ message = "Latitude must be between -90 and 90."
357
+ current_sample = SAMPLES[game_state["current_index"]]
358
+ clip_md = _prepare_clip_info(current_sample, game_state.get("round", 1))
359
+ prompt_text = message
360
+ return (
361
+ game_state,
362
+ guess_state,
363
+ str(current_sample.audio_path),
364
+ clip_md,
365
+ _base_map_array(),
366
+ prompt_text,
367
+ _format_recent_rows(_load_recent_runs()),
368
+ longitude,
369
+ latitude,
370
+ )
371
+ except ValueError:
372
+ message = "Invalid coordinates. Please enter valid numbers."
373
+ current_sample = SAMPLES[game_state["current_index"]]
374
+ clip_md = _prepare_clip_info(current_sample, game_state.get("round", 1))
375
+ prompt_text = message
376
+ return (
377
+ game_state,
378
+ guess_state,
379
+ str(current_sample.audio_path),
380
+ clip_md,
381
+ _base_map_array(),
382
+ prompt_text,
383
+ _format_recent_rows(_load_recent_runs()),
384
+ longitude,
385
+ latitude,
386
+ )
387
+
388
+ current_sample = SAMPLES[game_state["current_index"]]
389
+ true_lat = current_sample.latitude
390
+ true_lon = current_sample.longitude
391
+
392
+ distance_km = _haversine(true_lat, true_lon, guess_lat, guess_lon)
393
+ reveal_lines = [
394
+ f"Real location: {current_sample.city or 'Unknown city'}, {current_sample.country or 'Unknown country'} ({true_lat:.3f}°, {true_lon:.3f}°)",
395
+ f"Your guess: ({guess_lat:.3f}°, {guess_lon:.3f}°)",
396
+ f"Error distance: {distance_km:.1f} km",
397
+ ]
398
+
399
+ log_entry = {
400
+ "timestamp": dt.datetime.utcnow().isoformat(),
401
+ "player_id": player_id,
402
+ "question_id": current_sample.question_id,
403
+ "audio_path": str(current_sample.audio_path),
404
+ "guess_latitude": guess_lat,
405
+ "guess_longitude": guess_lon,
406
+ "true_latitude": true_lat,
407
+ "true_longitude": true_lon,
408
+ "distance_km": round(distance_km, 3),
409
+ "city": current_sample.city,
410
+ "country": current_sample.country,
411
+ "continent": current_sample.continent,
412
+ "title": current_sample.title,
413
+ "description": current_sample.description,
414
+ }
415
+ _append_log(log_entry)
416
+
417
+ next_sample = _next_sample(game_state)
418
+ audio_value = str(next_sample.audio_path)
419
+ clip_md = _prepare_clip_info(next_sample, game_state["round"])
420
+
421
+ new_guess_state = {"lat": None, "lon": None, "pixel": None}
422
+ prompt_text = "Click once on the map to pick your guess. The marker will update to your last selection."
423
+ recent_runs = _format_recent_rows(_load_recent_runs())
424
+
425
+ return (
426
+ game_state,
427
+ new_guess_state,
428
+ audio_value,
429
+ clip_md,
430
+ _base_map_array(),
431
+ prompt_text,
432
+ recent_runs,
433
+ "", # Reset longitude input
434
+ "", # Reset latitude input
435
+ )
436
+
437
+
438
+ custom_css = """
439
+ h1 {
440
+ font-size: 2.5rem !important;
441
+ font-weight: 700 !important;
442
+ margin-bottom: 1rem !important;
443
+ text-align: center !important;
444
+ }
445
+ .intro-text {
446
+ font-size: 1.1rem !important;
447
+ line-height: 1.6 !important;
448
+ margin-bottom: 1.5rem !important;
449
+ color: #555 !important;
450
+ text-align: center !important;
451
+ }
452
+ /* Apply size restrictions to regular (non-fullscreen) image */
453
+ .gradio-image:not([class*="fullscreen"]) {
454
+ max-width: 100% !important;
455
+ max-height: 600px !important;
456
+ margin: 0 auto !important;
457
+ display: flex !important;
458
+ justify-content: center !important;
459
+ }
460
+ .gradio-image:not([class*="fullscreen"]) > div {
461
+ max-width: 100% !important;
462
+ max-height: 600px !important;
463
+ margin: 0 auto !important;
464
+ display: flex !important;
465
+ justify-content: center !important;
466
+ }
467
+ .gradio-image:not([class*="fullscreen"]) img {
468
+ max-width: 100% !important;
469
+ max-height: 600px !important;
470
+ width: auto !important;
471
+ height: auto !important;
472
+ object-fit: contain !important;
473
+ margin: 0 auto !important;
474
+ }
475
+ /* Allow fullscreen modal to override size restrictions - higher specificity */
476
+ /* Target common Gradio fullscreen containers */
477
+ div[class*="modal"] .gradio-image,
478
+ div[class*="modal"] .gradio-image > div,
479
+ div[class*="modal"] .gradio-image img,
480
+ div[class*="fullscreen"] .gradio-image,
481
+ div[class*="fullscreen"] .gradio-image > div,
482
+ div[class*="fullscreen"] .gradio-image img,
483
+ div[id*="lightbox"] .gradio-image,
484
+ div[id*="lightbox"] .gradio-image img,
485
+ .gradio-image[class*="fullscreen"],
486
+ .gradio-image[class*="fullscreen"] > div,
487
+ .gradio-image[class*="fullscreen"] img {
488
+ max-width: none !important;
489
+ max-height: none !important;
490
+ width: 100% !important;
491
+ height: 100% !important;
492
+ }
493
+ .selection-text {
494
+ font-size: 1.15rem !important;
495
+ text-align: left !important;
496
+ color: #666 !important;
497
+ margin: 0.5rem 0 !important;
498
+ }
499
+ .selection-text * {
500
+ font-size: 1.15rem !important;
501
+ }
502
+ .feedback-box {
503
+ font-size: 1.15rem !important;
504
+ line-height: 1.6 !important;
505
+ }
506
+ .feedback-box p,
507
+ .feedback-box div,
508
+ .feedback-box span {
509
+ font-size: 1.15rem !important;
510
+ line-height: 1.6 !important;
511
+ }
512
+ .clip-info {
513
+ font-size: 1.1rem !important;
514
+ }
515
+ .clip-info p,
516
+ .clip-info div,
517
+ .clip-info span {
518
+ font-size: 1.1rem !important;
519
+ }
520
+ .clip-info label {
521
+ font-size: 1.1rem !important;
522
+ font-weight: 600 !important;
523
+ }
524
+ label {
525
+ font-size: 1rem !important;
526
+ font-weight: 500 !important;
527
+ }
528
+ .form-text input {
529
+ font-size: 0.95rem !important;
530
+ }
531
+ /* Table label styling - only target the label, not table content */
532
+ .gradio-dataframe label,
533
+ [data-testid="dataframe"] label,
534
+ label[for*="recent"],
535
+ .form-group label {
536
+ font-size: 1.15rem !important;
537
+ font-weight: 600 !important;
538
+ }
539
+ """
540
+
541
+ with gr.Blocks(title="Audio Geo-Localization Game", theme=gr.themes.Soft(), css=custom_css) as demo:
542
+ gr.Markdown("# Audio Geo-Localization Game")
543
+ gr.HTML('<p class="intro-text">Welcome to the Audio Geo-Localization Game. Listen to an ambient audio clip, then guess where it was recorded by clicking on the world map. Submit to see the true location and how close you came.</p>')
544
+
545
+ game_state = gr.State()
546
+ guess_state = gr.State()
547
+
548
+
549
+ clip_info = gr.Markdown(label="Clip details", elem_classes=["clip-info"])
550
+
551
+
552
+ with gr.Row():
553
+ with gr.Column(scale=2):
554
+ map_component = gr.Image(
555
+ value=_base_map_array(),
556
+ label="World map (click to set your guess)",
557
+ interactive=True,
558
+ type="numpy",
559
+ image_mode="RGB",
560
+ elem_classes=["gradio-image"],
561
+ )
562
+
563
+ with gr.Column(scale=1):
564
+ player_id = gr.Textbox(label="Player ID", placeholder="Enter an identifier so scores can be tracked", elem_classes=["form-text"])
565
+
566
+ audio_player = gr.Audio(label="Mystery audio clip", autoplay=False, interactive=False, streaming=False)
567
+
568
+ selection_text = gr.Markdown("Click once on the map to pick your guess. The marker will update to your last selection.", elem_classes=["selection-text"])
569
+
570
+ with gr.Row():
571
+ longitude_input = gr.Textbox(
572
+ label="Longitude",
573
+ placeholder="Enter longitude (-180 to 180) or click map",
574
+ elem_classes=["form-text"]
575
+ )
576
+ latitude_input = gr.Textbox(
577
+ label="Latitude",
578
+ placeholder="Enter latitude (-90 to 90) or click map",
579
+ elem_classes=["form-text"]
580
+ )
581
+
582
+ submit_button = gr.Button("Submit Guess", variant="primary")
583
+ recent_table = gr.Dataframe(
584
+ headers=[
585
+ "timestamp",
586
+ "player_id",
587
+ "question_id",
588
+ "distance_km",
589
+ ],
590
+ datatype=["str", "str", "str", "number"],
591
+ value=[],
592
+ interactive=False,
593
+ label="Recent submissions (latest last)",
594
+ wrap=True,
595
+ )
596
+
597
+ demo.load(
598
+ initialize_round,
599
+ inputs=None,
600
+ outputs=[game_state, guess_state, audio_player, clip_info, map_component, selection_text, recent_table, longitude_input, latitude_input],
601
+ )
602
+
603
+ map_component.select(
604
+ handle_map_click,
605
+ inputs=[guess_state],
606
+ outputs=[map_component, selection_text, guess_state, longitude_input, latitude_input],
607
+ preprocess=False,
608
+ )
609
+
610
+ submit_button.click(
611
+ submit_guess,
612
+ inputs=[player_id, game_state, guess_state, longitude_input, latitude_input],
613
+ outputs=[game_state, guess_state, audio_player, clip_info, map_component, selection_text, recent_table, longitude_input, latitude_input],
614
+ )
615
+
616
+
617
+ if __name__ == "__main__":
618
+ demo.launch(server_name="0.0.0.0", server_port=3828)
data/dataset.csv ADDED
The diff for this file is too large to render. See raw diff
 
player_runs.csv ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ timestamp,player_id,question_id,audio_path,guess_latitude,guess_longitude,true_latitude,true_longitude,distance_km,city,country,continent,title,description
2
+ 2025-11-04T17:13:12.639892,233,aporee_2086_3020,/home/zhangrx/SoundingEarth/data/audios/heiligenhausflugfeld.mp3,40.147059,100.332193,51.3011765169163,6.95689380168915,6853.735,Heiligenhaus,Germany,Europe,"heiligenhaus, wiel, gang durch feld","gang durch durchfrostetes feld , morgens,<br />nahe flugfeld<br /><br /><br /><br />project: :resonanzen - neanderland - soundmap nrw: social ambiences/ listen to the people - in & outdoor nearfield recordings, listen to the people"
3
+ 2025-11-05T15:48:08.753807,2333,aporee_8931_10771,/home/zhangrx/SoundingEarth/data/audios/potiltu.mp3,28.823529,14.509038,55.5165748413,25.6533551216,3098.156,Utena,Lithuania,Europe,"Lithuania, Utena, under the bridge",sitting under the bridge as cars go above
4
+ 2025-11-05T15:48:15.147956,2333,aporee_18759_21770,/home/zhangrx/SoundingEarth/data/audios/SoundMap2012032912.mp3,-5.294118,50.913532,22.76054838793,120.30957341194,8150.178,Fengshan,"Taiwan, Province of China",Asia,"Ciaotou,Kaohsiung - Corner","Traffic Noise,Sony ECM-MS907,Sony Hi-MD MZ-RH1 (SoundMap20120329-12), recorded by Wu,Tsancheng"
5
+ 2025-11-05T15:48:44.673310,2333,aporee_6623_8181,/home/zhangrx/SoundingEarth/data/audios/waterbubbles44.mp3,-15.441176,32.623351,54.1275688685071,-6.00030541419983,8558.199,Kilkeel,United Kingdom,Europe,Silent Valley tunnel recording 2,Liz Greene and Rui Chaves recording
6
+ 2025-11-05T15:55:29.468345,2333,aporee_7014_8710,/home/zhangrx/SoundingEarth/data/audios/orchestrion37.mp3,-11.617647,-26.995603,45.8132359846,15.9762561321,7694.498,Zagreb - Centar,Croatia,Europe,Trg Bana Josipa Jelacica,"street organ [orchestrion], with the sound of the church bells. EBU workshop"
7
+ 2025-11-05T15:55:41.977995,2333,aporee_41776_47627,/home/zhangrx/SoundingEarth/data/audios/pitcher.mp3,-0.294118,-11.167562,35.69359789993,139.70536802721,15059.477,Tokyo,Japan,Asia,"5 Chome-17-3 Shinjuku, 新宿区 Tokyo 160-0022, Japonia - rain colector",recorder held in a massive metal pitcher used for collecting rain water. it resonates beautifully when rain drops hit the surface of the water. (roland r-07)
8
+ 2025-11-05T16:06:48.987419,2333,aporee_17575_20453,/home/zhangrx/SoundingEarth/data/audios/avignonbellG.mp3,-1.470588,-64.982902,43.951734380678,4.8068779706955,8523.639,Avignon,France,Europe,"Avignon, France - Avignon Bell","Bell from the Cathedral.<br />Stereo mic, cassette recorder<br />a sound from Raw Materials project - http://aporee.org/maps/projects/rawmaterials"
9
+ 2025-11-05T16:06:53.948731,2333,aporee_10545_12538,/home/zhangrx/SoundingEarth/data/audios/R090004preslova.mp3,4.558824,-75.007328,49.1964916699,16.5780258179,9738.971,Brno,Czechia,Europe,"Awake at Preslova, Brno","awake of the city. this is a general soundscape of every awake, no surprise: birds."
10
+ 2025-11-05T16:07:00.555047,23345,aporee_9802_11738,/home/zhangrx/SoundingEarth/data/audios/conduit.mp3,5.147059,-102.9702,59.4394091209,24.7352698445,11502.248,Tallinn,Estonia,Europe,"Tallinn, Baltijaam platiform electrical conduit - Tallinn, Baltijaam platiform electrical conduit (recorded w/ kessu karu)","hidden sounds, plastic scraps blowing against an electrical conduit on a platform pole at Tallinns main train station."
11
+ 2025-11-05T16:08:23.088089,sdsdsds,aporee_19250_22366,/home/zhangrx/SoundingEarth/data/audios/berlinMauerwegWohlgemuthstrBagger170813.mp3,18.529412,-127.767465,52.463416953564,13.478784263134,11280.679,Baumschulenweg,Germany,Europe,"Mauerweg, Wohlgemuthstr., Berlin - demolition of buildings","Saturday, noon, Mauerweg (former Berlin wall area), Wohlgemuthstr., demolition of garages, reflections and drone of excavator<br />(SD702, Audio Technica BP4025)"
12
+ 2025-11-05T18:03:57.068732,hello,aporee_30969_35607,/home/zhangrx/SoundingEarth/gradioFrontend/data/audios/BruceHillsScorpionCricket.mp3,42.058824,15.212506,42.809569805154,-83.054065704346,7545.338,Romeo,United States,North America,"Bruce Township, MI, USA - Scorpion Cricket in Rural Michigan","Recorded in Summer of 2014, this is what I believe to be some type of large cricket, but hopefully somebody can identify the sound for me? It was captured on a dark, desolate dirt road outside of Romeo, MI, at around 10:30 pm with calm conditions. I used a Tascam dr-07 and a tripod, leaving the mic aimed right at the creature, just past the ditch on the side of the road.<br /><br />Many more field recordings, sound collages, and mixtapes available on soundcloud:<br />https://soundcloud.com/rjsfoundsounds"
13
+ 2025-11-05T18:04:11.487793,hello,aporee_2104_12272,/home/zhangrx/SoundingEarth/gradioFrontend/data/audios/buerknerBackyardRain310511.mp3,-0.294118,48.099658,52.4934062608,13.4243441373,6696.422,Berlin Treptow,Germany,Europe,"Berlin Bürknerstr., backyard window - spring day, it starts to rain",it starts to rain after a warm early summer day.
14
+ 2025-11-05T18:04:13.320117,hello,aporee_6139_7665,/home/zhangrx/SoundingEarth/gradioFrontend/data/audios/staircase20100202.mp3,31.617647,38.075232,52.5224000252555,13.4084218740463,3059.458,Berlin,Germany,Europe,Alexanderplatz - Staircase,Staircase near Berlin Alexanderplatz. Quite windy. Aporee Workshop CTM.