cjc0013 commited on
Commit
e94095f
·
verified ·
1 Parent(s): 529797d

Make drone Space map-first

Browse files

Replace chart-first triage surface with a map-first Space plotting all 149 cases, including coordinate-quality labels and source-detail table.

README.md CHANGED
@@ -11,6 +11,6 @@ python_version: 3.11
11
 
12
  # Mystery Drone Reports Around Sensitive Sites
13
 
14
- Interactive review surface for public-source reports about mystery, unidentified, suspicious, or unauthorized drone activity around sensitive sites.
15
 
16
- This Space presents evidence tiers, source links, date signals, country/site filters, and row-level claim boundaries. It does not claim that any row proves threat, attribution, anomalous origin, or hostile intent.
 
11
 
12
  # Mystery Drone Reports Around Sensitive Sites
13
 
14
+ Map-first review surface for public-source reports about mystery, unidentified, suspicious, or unauthorized drone activity around sensitive sites.
15
 
16
+ This Space plots the expanded case set on a world map, with evidence tiers, source links, coordinate-quality labels, country/site filters, and row-level claim boundaries. It does not claim that any row proves threat, attribution, anomalous origin, or hostile intent.
data/mystery_drone_sensitive_site_cases.csv CHANGED
The diff for this file is too large to render. See raw diff
 
data/mystery_drone_sensitive_site_cases.jsonl CHANGED
The diff for this file is too large to render. See raw diff
 
data/quality_report.json CHANGED
@@ -7,6 +7,8 @@
7
  "missing_source_url_count": 0,
8
  "duplicate_source_url_count": 0,
9
  "generic_headline_count": 0,
 
 
10
  "forbidden_public_term_hits": [],
11
  "drone_signal_count": 149,
12
  "sensitive_signal_count": 148,
 
7
  "missing_source_url_count": 0,
8
  "duplicate_source_url_count": 0,
9
  "generic_headline_count": 0,
10
+ "mappable_case_count": 149,
11
+ "unresolved_global_fallback_count": 0,
12
  "forbidden_public_term_hits": [],
13
  "drone_signal_count": 149,
14
  "sensitive_signal_count": 148,
data/release_manifest.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "release_version": "mystery-drone-sensitive-site-cases-2026-05-v1",
3
  "title": "Mystery Drone Reports Around Sensitive Sites",
4
- "generated_utc": "2026-05-01T20:32:52-04:00",
5
  "source_lead_count": 150,
6
  "case_count": 149,
7
  "resolved_sensitive_site_report_count": 9,
@@ -28,21 +28,21 @@
28
  "source_discovered_report": 129
29
  },
30
  "claim_boundary": "Public-source report index; not a verified finding of threat, attribution, or anomalous origin.",
31
- "summary_hash": "5285f25aeaff138958731d3eda485b925682ed50a2742c3c319cb5349cd92c47",
32
  "release_grade": true,
33
  "quality_failed_checks": [],
34
  "artifacts": [
35
  {
36
  "artifact_role": "cases_csv",
37
  "artifact_path": "data/mystery_drone_sensitive_site_cases.csv",
38
- "content_sha256": "62c2aba1b5708d7223ac6c6b3dcb94a777d4289dab73f521f93fa6baa6480aa6",
39
- "byte_count": 105504
40
  },
41
  {
42
  "artifact_role": "cases_jsonl",
43
  "artifact_path": "data/mystery_drone_sensitive_site_cases.jsonl",
44
- "content_sha256": "53546245b4b553861f30d9565a20645d7828a348b8e6e9280d26c327432f024d",
45
- "byte_count": 164513
46
  },
47
  {
48
  "artifact_role": "readme",
@@ -71,9 +71,9 @@
71
  {
72
  "artifact_role": "quality_report",
73
  "artifact_path": "quality_report.json",
74
- "content_sha256": "a8f2e692f08bd8d5fef88d00691d018f6fa0b28b2f550da995d3b2eb1f9072b3",
75
- "byte_count": 935
76
  }
77
  ],
78
- "manifest_hash": "cd861520adba5e1ead213fd6389208eeea2d5d1041c1590bbdd7ddca1525cfbb"
79
  }
 
1
  {
2
  "release_version": "mystery-drone-sensitive-site-cases-2026-05-v1",
3
  "title": "Mystery Drone Reports Around Sensitive Sites",
4
+ "generated_utc": "2026-05-01T20:52:07-04:00",
5
  "source_lead_count": 150,
6
  "case_count": 149,
7
  "resolved_sensitive_site_report_count": 9,
 
28
  "source_discovered_report": 129
29
  },
30
  "claim_boundary": "Public-source report index; not a verified finding of threat, attribution, or anomalous origin.",
31
+ "summary_hash": "5b77a3c7a1a911bbfe43907912230479392387942073ed57f029f33df1a48e79",
32
  "release_grade": true,
33
  "quality_failed_checks": [],
34
  "artifacts": [
35
  {
36
  "artifact_role": "cases_csv",
37
  "artifact_path": "data/mystery_drone_sensitive_site_cases.csv",
38
+ "content_sha256": "c4c2ccd98afa495171a2dd30563e2f8812941c646090e6e930fa9c06285219f9",
39
+ "byte_count": 114576
40
  },
41
  {
42
  "artifact_role": "cases_jsonl",
43
  "artifact_path": "data/mystery_drone_sensitive_site_cases.jsonl",
44
+ "content_sha256": "50f5f9837ec9832e5c5f380bdd45d4e0176cae94f1f0aa346390a3eb7ab33fa0",
45
+ "byte_count": 183665
46
  },
47
  {
48
  "artifact_role": "readme",
 
71
  {
72
  "artifact_role": "quality_report",
73
  "artifact_path": "quality_report.json",
74
+ "content_sha256": "b17691a650a7a913224143e660f6049502c4d6077c5a5d06cc32b83ddb12b0ad",
75
+ "byte_count": 1008
76
  }
77
  ],
78
+ "manifest_hash": "f6b93171f75323da37d79c9d18fe48ee8dca5ae066c5b14c689d3450b9bd63b4"
79
  }
public_space_app.py CHANGED
@@ -15,6 +15,7 @@ DISPLAY_COLUMNS = [
15
  "country",
16
  "site_name",
17
  "site_type",
 
18
  "headline",
19
  "source_domain",
20
  "followup_status",
@@ -35,7 +36,7 @@ def _markdown_header(manifest: dict, quality: dict) -> str:
35
  top_countries = ", ".join(f"{key}: {value}" for key, value in list(countries.items())[:7])
36
  return f"""# Mystery Drone Reports Around Sensitive Sites
37
 
38
- Public-source review surface for mystery, unidentified, suspicious, or unauthorized drone reports around military, airport, maritime, emergency-service, and critical-infrastructure contexts.
39
 
40
  **{manifest.get("case_count", 0)} cases** | **{manifest.get("probable_cluster_count", 0)} probable clusters** | **release gate: {"pass" if quality.get("release_grade") else "review"}**
41
 
@@ -43,7 +44,7 @@ Evidence tiers: resolved sensitive-site reports `{tiers.get("resolved_sensitive_
43
 
44
  Country coverage: {top_countries}
45
 
46
- Rows are source-indexed report cases, not verified findings of threat, attribution, anomalous origin, or hostile intent.
47
  """
48
 
49
 
@@ -90,41 +91,39 @@ def _summary_text(filtered: pd.DataFrame) -> str:
90
  return f"Showing {len(filtered)} cases. Evidence tiers: {tiers}. Top countries: {countries}."
91
 
92
 
93
- def _timeline(filtered: pd.DataFrame):
94
  if filtered.empty:
95
- return px.bar(pd.DataFrame({"report_year": [], "count": []}), x="report_year", y="count", height=320)
96
- chart_data = (
97
- filtered.groupby(["report_year", "evidence_tier"], dropna=False)
98
- .size()
99
- .reset_index(name="count")
100
- .sort_values(["report_year", "evidence_tier"])
101
- )
102
- fig = px.bar(
103
- chart_data,
104
- x="report_year",
105
- y="count",
106
  color="evidence_tier",
107
- height=320,
108
- labels={"report_year": "Report year", "count": "Cases", "evidence_tier": "Evidence tier"},
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  )
110
- fig.update_layout(margin={"l": 10, "r": 10, "t": 24, "b": 10}, legend_orientation="h")
111
- return fig
112
-
113
-
114
- def _country_chart(filtered: pd.DataFrame):
115
- if filtered.empty:
116
- return px.bar(pd.DataFrame({"country": [], "count": []}), x="count", y="country", height=320)
117
- chart_data = filtered["country"].replace("", "unknown").value_counts().head(15).reset_index()
118
- chart_data.columns = ["country", "count"]
119
- fig = px.bar(
120
- chart_data,
121
- x="count",
122
- y="country",
123
- orientation="h",
124
- height=320,
125
- labels={"country": "Country", "count": "Cases"},
126
- )
127
- fig.update_layout(margin={"l": 10, "r": 10, "t": 24, "b": 10}, yaxis={"categoryorder": "total ascending"})
128
  return fig
129
 
130
 
@@ -134,7 +133,8 @@ def _table(filtered: pd.DataFrame) -> pd.DataFrame:
134
 
135
  def _render(cases: pd.DataFrame, evidence_tiers, countries, site_types, query):
136
  filtered = _filter_cases(cases, evidence_tiers, countries, site_types, query)
137
- return _summary_text(filtered), _timeline(filtered), _country_chart(filtered), _table(filtered), filtered.to_dict("records"), _detail(filtered.to_dict("records"), 0)
 
138
 
139
 
140
  def _detail(rows: list[dict], index: int | None) -> str:
@@ -150,6 +150,7 @@ def _detail(rows: list[dict], index: int | None) -> str:
150
  - Follow-up status: `{row.get("followup_status", "")}`
151
  - Report date: `{row.get("report_date", "")}` (`{row.get("date_quality", "")}`)
152
  - Site signal: `{row.get("site_name", "")}` / `{row.get("site_type", "")}`
 
153
  - Location signal: `{row.get("country", "")}` `{row.get("state_region", "")}`
154
  - Source: [{row.get("publisher", "") or row.get("source_domain", "")}]({row.get("source_url", "")})
155
  - Boundary: {row.get("claim_boundary", "")}
@@ -182,9 +183,7 @@ def build_app(data_dir: str | Path):
182
  )
183
  query = gr.Textbox(label="Search", placeholder="Try Langley, Copenhagen, airport, military base")
184
  summary = gr.Markdown()
185
- with gr.Row():
186
- timeline = gr.Plot(label="Cases by report year")
187
- country_chart = gr.Plot(label="Cases by country")
188
  table = gr.Dataframe(label="Filtered cases", interactive=False)
189
  rows_state = gr.State([])
190
  detail = gr.Markdown()
@@ -196,7 +195,7 @@ def build_app(data_dir: str | Path):
196
  control.change(
197
  render,
198
  inputs=[evidence_filter, country_filter, site_filter, query],
199
- outputs=[summary, timeline, country_chart, table, rows_state, detail],
200
  )
201
 
202
  def select_detail(rows, evt: gr.SelectData):
@@ -209,6 +208,6 @@ def build_app(data_dir: str | Path):
209
  app.load(
210
  render,
211
  inputs=[evidence_filter, country_filter, site_filter, query],
212
- outputs=[summary, timeline, country_chart, table, rows_state, detail],
213
  )
214
  return app
 
15
  "country",
16
  "site_name",
17
  "site_type",
18
+ "coordinate_quality",
19
  "headline",
20
  "source_domain",
21
  "followup_status",
 
36
  top_countries = ", ".join(f"{key}: {value}" for key, value in list(countries.items())[:7])
37
  return f"""# Mystery Drone Reports Around Sensitive Sites
38
 
39
+ Map-first public review surface for mystery, unidentified, suspicious, or unauthorized drone reports around military, airport, maritime, emergency-service, and critical-infrastructure contexts.
40
 
41
  **{manifest.get("case_count", 0)} cases** | **{manifest.get("probable_cluster_count", 0)} probable clusters** | **release gate: {"pass" if quality.get("release_grade") else "review"}**
42
 
 
44
 
45
  Country coverage: {top_countries}
46
 
47
+ Rows are source-indexed report cases, not verified findings of threat, attribution, anomalous origin, or hostile intent. Map points use the best public coordinate available: site centroid, city/region centroid, country centroid, or a clearly labeled fallback.
48
  """
49
 
50
 
 
91
  return f"Showing {len(filtered)} cases. Evidence tiers: {tiers}. Top countries: {countries}."
92
 
93
 
94
+ def _map(filtered: pd.DataFrame):
95
  if filtered.empty:
96
+ fig = px.scatter_geo(pd.DataFrame({"plot_lat": [], "plot_lon": []}), lat="plot_lat", lon="plot_lon", height=620)
97
+ fig.update_layout(margin={"l": 0, "r": 0, "t": 20, "b": 0})
98
+ return fig
99
+ plot_rows = filtered.copy()
100
+ plot_rows["plot_lat"] = pd.to_numeric(plot_rows["plot_lat"], errors="coerce")
101
+ plot_rows["plot_lon"] = pd.to_numeric(plot_rows["plot_lon"], errors="coerce")
102
+ plot_rows = plot_rows.dropna(subset=["plot_lat", "plot_lon"])
103
+ fig = px.scatter_geo(
104
+ plot_rows,
105
+ lat="plot_lat",
106
+ lon="plot_lon",
107
  color="evidence_tier",
108
+ symbol="coordinate_quality",
109
+ hover_name="headline",
110
+ hover_data={
111
+ "case_rank": True,
112
+ "site_name": True,
113
+ "plot_label": True,
114
+ "country": True,
115
+ "report_date": True,
116
+ "source_domain": True,
117
+ "coordinate_quality": True,
118
+ "plot_lat": False,
119
+ "plot_lon": False,
120
+ },
121
+ projection="natural earth",
122
+ height=660,
123
  )
124
+ fig.update_traces(marker={"size": 9, "opacity": 0.78, "line": {"width": 0.4, "color": "white"}})
125
+ fig.update_geos(showland=True, landcolor="#eef2f5", showocean=True, oceancolor="#dfeaf2", showcountries=True)
126
+ fig.update_layout(margin={"l": 0, "r": 0, "t": 24, "b": 0}, legend_orientation="h")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  return fig
128
 
129
 
 
133
 
134
  def _render(cases: pd.DataFrame, evidence_tiers, countries, site_types, query):
135
  filtered = _filter_cases(cases, evidence_tiers, countries, site_types, query)
136
+ rows = filtered.to_dict("records")
137
+ return _summary_text(filtered), _map(filtered), _table(filtered), rows, _detail(rows, 0)
138
 
139
 
140
  def _detail(rows: list[dict], index: int | None) -> str:
 
150
  - Follow-up status: `{row.get("followup_status", "")}`
151
  - Report date: `{row.get("report_date", "")}` (`{row.get("date_quality", "")}`)
152
  - Site signal: `{row.get("site_name", "")}` / `{row.get("site_type", "")}`
153
+ - Map point: `{row.get("plot_label", "")}` / `{row.get("coordinate_quality", "")}`
154
  - Location signal: `{row.get("country", "")}` `{row.get("state_region", "")}`
155
  - Source: [{row.get("publisher", "") or row.get("source_domain", "")}]({row.get("source_url", "")})
156
  - Boundary: {row.get("claim_boundary", "")}
 
183
  )
184
  query = gr.Textbox(label="Search", placeholder="Try Langley, Copenhagen, airport, military base")
185
  summary = gr.Markdown()
186
+ map_plot = gr.Plot(label="Case map")
 
 
187
  table = gr.Dataframe(label="Filtered cases", interactive=False)
188
  rows_state = gr.State([])
189
  detail = gr.Markdown()
 
195
  control.change(
196
  render,
197
  inputs=[evidence_filter, country_filter, site_filter, query],
198
+ outputs=[summary, map_plot, table, rows_state, detail],
199
  )
200
 
201
  def select_detail(rows, evt: gr.SelectData):
 
208
  app.load(
209
  render,
210
  inputs=[evidence_filter, country_filter, site_filter, query],
211
+ outputs=[summary, map_plot, table, rows_state, detail],
212
  )
213
  return app
space_manifest.json CHANGED
@@ -13,14 +13,14 @@
13
  {
14
  "artifact_role": "space_public_app",
15
  "artifact_path": "public_space_app.py",
16
- "content_sha256": "7e3e75670d058329a171ad3c8a5e94a7f16180648ff42a14fc2fdae73db0d44c",
17
- "byte_count": 8719
18
  },
19
  {
20
  "artifact_role": "readme",
21
  "artifact_path": "README.md",
22
- "content_sha256": "7b777472f3186a1897a48733a178809b4b7c31bc3c7232ed485beef2322a893b",
23
- "byte_count": 575
24
  },
25
  {
26
  "artifact_role": "requirements",
@@ -31,21 +31,21 @@
31
  {
32
  "artifact_role": "cases_csv",
33
  "artifact_path": "data/mystery_drone_sensitive_site_cases.csv",
34
- "content_sha256": "62c2aba1b5708d7223ac6c6b3dcb94a777d4289dab73f521f93fa6baa6480aa6",
35
- "byte_count": 105504
36
  },
37
  {
38
  "artifact_role": "release_manifest",
39
  "artifact_path": "data/release_manifest.json",
40
- "content_sha256": "7949299f4fc596f268adcf4f97834c0aa8f2ef90c00342b2b2c8d28143aff276",
41
- "byte_count": 2772
42
  },
43
  {
44
  "artifact_role": "quality_report",
45
  "artifact_path": "data/quality_report.json",
46
- "content_sha256": "a8f2e692f08bd8d5fef88d00691d018f6fa0b28b2f550da995d3b2eb1f9072b3",
47
- "byte_count": 935
48
  }
49
  ],
50
- "bundle_hash": "7643bacb21b3784e4117c48f4858374d174e480e416569c7913a9553af226619"
51
  }
 
13
  {
14
  "artifact_role": "space_public_app",
15
  "artifact_path": "public_space_app.py",
16
+ "content_sha256": "fa5bdda74630f425a0389dbb89ba43ab1bc81d9302a5a0feeb657d5146ccd172",
17
+ "byte_count": 8877
18
  },
19
  {
20
  "artifact_role": "readme",
21
  "artifact_path": "README.md",
22
+ "content_sha256": "3dc884fb6fe90ae55d6494c38e365e0660dc68efb2519d8e54f7c14fc19e2669",
23
+ "byte_count": 626
24
  },
25
  {
26
  "artifact_role": "requirements",
 
31
  {
32
  "artifact_role": "cases_csv",
33
  "artifact_path": "data/mystery_drone_sensitive_site_cases.csv",
34
+ "content_sha256": "c4c2ccd98afa495171a2dd30563e2f8812941c646090e6e930fa9c06285219f9",
35
+ "byte_count": 114576
36
  },
37
  {
38
  "artifact_role": "release_manifest",
39
  "artifact_path": "data/release_manifest.json",
40
+ "content_sha256": "6e44306e314c2b3f7350cf53a65fc42b3ac0a92fd1d2f21b95b812544dcf85ee",
41
+ "byte_count": 2773
42
  },
43
  {
44
  "artifact_role": "quality_report",
45
  "artifact_path": "data/quality_report.json",
46
+ "content_sha256": "b17691a650a7a913224143e660f6049502c4d6077c5a5d06cc32b83ddb12b0ad",
47
+ "byte_count": 1008
48
  }
49
  ],
50
+ "bundle_hash": "89aeb842010180e667afc7a238b888daaf1480ac29ed298735b5f454c67e107b"
51
  }