Spaces:
Running
Running
Make drone Space map-first
Browse filesReplace chart-first triage surface with a map-first Space plotting all 149 cases, including coordinate-quality labels and source-detail table.
- README.md +2 -2
- data/mystery_drone_sensitive_site_cases.csv +0 -0
- data/mystery_drone_sensitive_site_cases.jsonl +0 -0
- data/quality_report.json +2 -0
- data/release_manifest.json +9 -9
- public_space_app.py +39 -40
- space_manifest.json +11 -11
README.md
CHANGED
|
@@ -11,6 +11,6 @@ python_version: 3.11
|
|
| 11 |
|
| 12 |
# Mystery Drone Reports Around Sensitive Sites
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
This Space
|
|
|
|
| 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:
|
| 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": "
|
| 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": "
|
| 39 |
-
"byte_count":
|
| 40 |
},
|
| 41 |
{
|
| 42 |
"artifact_role": "cases_jsonl",
|
| 43 |
"artifact_path": "data/mystery_drone_sensitive_site_cases.jsonl",
|
| 44 |
-
"content_sha256": "
|
| 45 |
-
"byte_count":
|
| 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": "
|
| 75 |
-
"byte_count":
|
| 76 |
}
|
| 77 |
],
|
| 78 |
-
"manifest_hash": "
|
| 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 |
-
|
| 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
|
| 94 |
if filtered.empty:
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
)
|
| 102 |
-
fig = px.
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
color="evidence_tier",
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
)
|
| 110 |
-
fig.
|
| 111 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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,
|
| 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,
|
| 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": "
|
| 17 |
-
"byte_count":
|
| 18 |
},
|
| 19 |
{
|
| 20 |
"artifact_role": "readme",
|
| 21 |
"artifact_path": "README.md",
|
| 22 |
-
"content_sha256": "
|
| 23 |
-
"byte_count":
|
| 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": "
|
| 35 |
-
"byte_count":
|
| 36 |
},
|
| 37 |
{
|
| 38 |
"artifact_role": "release_manifest",
|
| 39 |
"artifact_path": "data/release_manifest.json",
|
| 40 |
-
"content_sha256": "
|
| 41 |
-
"byte_count":
|
| 42 |
},
|
| 43 |
{
|
| 44 |
"artifact_role": "quality_report",
|
| 45 |
"artifact_path": "data/quality_report.json",
|
| 46 |
-
"content_sha256": "
|
| 47 |
-
"byte_count":
|
| 48 |
}
|
| 49 |
],
|
| 50 |
-
"bundle_hash": "
|
| 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 |
}
|