cwadayi commited on
Commit
1022bf0
·
verified ·
1 Parent(s): f368338

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +97 -64
app.py CHANGED
@@ -1,39 +1,40 @@
1
  import os
2
  import io
3
  import time
 
4
  import tempfile
5
  from typing import Optional, Tuple, List
6
 
7
  import matplotlib.pyplot as plt
8
  import gradio as gr
9
 
10
- # Google Drive (service account)
11
  from googleapiclient.discovery import build
12
  from googleapiclient.http import MediaIoBaseUpload
 
13
  from google.oauth2.service_account import Credentials
14
 
15
 
16
- # --------- Config ---------
17
  DEFAULT_EXTENT = (119.0, 123.5, 21.5, 25.5) # (min_lon, max_lon, min_lat, max_lat) ~ Taiwan region
18
- SCOPES = ["https://www.googleapis.com/auth/drive.file"] # minimal scope to create files you own
19
 
20
 
 
21
  def _parse_points(csv_text: str) -> List[Tuple[float, float]]:
22
  """
23
  Accepts lines like:
24
  121.5,25.0
25
  120.9,24.5
26
- 121.0,23.9
27
- (lon,lat). Ignores blank/comment lines.
28
  """
29
- pts = []
30
  if not csv_text:
31
  return pts
32
  for ln in csv_text.strip().splitlines():
33
  ln = ln.strip()
34
  if not ln or ln.startswith("#"):
35
  continue
36
- # allow "lon,lat" or "lat,lon" if user toggles a flag later (we keep lon,lat only here for simplicity)
37
  parts = [p.strip() for p in ln.replace("\t", ",").split(",")]
38
  if len(parts) < 2:
39
  continue
@@ -42,7 +43,6 @@ def _parse_points(csv_text: str) -> List[Tuple[float, float]]:
42
  lat = float(parts[1])
43
  pts.append((lon, lat))
44
  except ValueError:
45
- # skip invalid rows
46
  continue
47
  return pts
48
 
@@ -54,43 +54,34 @@ def _make_figure(
54
  dpi: int = 150,
55
  ) -> str:
56
  """
57
- Draw a simple lon/lat scatter within the extent.
58
- Returns path to the saved PNG.
59
  """
60
  min_lon, max_lon, min_lat, max_lat = extent
61
  fig, ax = plt.subplots(figsize=(6.5, 6.5), dpi=dpi)
62
  ax.set_title(title or "Map Plot", pad=10)
63
  ax.set_xlabel("Longitude")
64
  ax.set_ylabel("Latitude")
65
-
66
- # Axes extent
67
  ax.set_xlim(min_lon, max_lon)
68
  ax.set_ylim(min_lat, max_lat)
69
  ax.grid(True, linestyle=":", linewidth=0.8)
70
 
71
- # Light background box suggesting region
72
- rect = plt.Rectangle(
73
- (min_lon, min_lat),
74
- max_lon - min_lon,
75
- max_lat - min_lat,
76
- fill=False,
77
- linestyle="--",
78
- linewidth=1.0,
79
- )
80
- ax.add_patch(rect)
81
 
82
- # Plot points
83
  if points:
84
  xs = [p[0] for p in points]
85
  ys = [p[1] for p in points]
86
  ax.scatter(xs, ys, s=60, marker="*", label="Input points")
87
  ax.legend(loc="upper right")
88
 
89
- # Save to a temporary PNG
90
  tmpdir = tempfile.gettempdir()
91
  ts = int(time.time())
92
- filename = f"map_{ts}.png"
93
- out_path = os.path.join(tmpdir, filename)
94
  fig.tight_layout()
95
  fig.savefig(out_path)
96
  plt.close(fig)
@@ -99,34 +90,30 @@ def _make_figure(
99
 
100
  def _drive_service_from_env() -> Optional[object]:
101
  """
102
- Build a Google Drive service using a service account JSON
103
- provided via the Hugging Face Space secret `SERVICE_ACCOUNT_JSON`.
104
  """
105
  sa_json = os.environ.get("SERVICE_ACCOUNT_JSON", "").strip()
106
  if not sa_json:
107
  return None
108
-
109
- # In Spaces secrets, you typically paste the **full** JSON string of the service account.
110
- # Load it into Credentials directly via from_service_account_info.
111
- import json
112
  try:
113
  info = json.loads(sa_json)
 
 
 
 
114
  except Exception:
115
  return None
116
 
117
- creds = Credentials.from_service_account_info(info, scopes=SCOPES)
118
- service = build("drive", "v3", credentials=creds, cache_discovery=False)
119
- return service
120
-
121
 
122
  def _upload_to_drive(
123
  local_path: str,
124
  service,
125
  folder_id: Optional[str] = None,
 
126
  ) -> Tuple[str, str]:
127
  """
128
- Uploads local file to Google Drive.
129
- Returns (file_id, webViewLink)
130
  """
131
  fname = os.path.basename(local_path)
132
  file_metadata = {"name": fname}
@@ -137,15 +124,35 @@ def _upload_to_drive(
137
  media = MediaIoBaseUpload(f, mimetype="image/png", resumable=False)
138
  created = (
139
  service.files()
140
- .create(body=file_metadata, media_body=media, fields="id, webViewLink, webContentLink")
 
 
 
 
 
141
  .execute()
142
  )
143
 
144
  file_id = created.get("id", "")
145
- web_view_link = created.get("webViewLink", "")
146
- return file_id, web_view_link
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
 
 
149
  def generate_and_upload(
150
  points_text: str,
151
  title: str,
@@ -154,38 +161,49 @@ def generate_and_upload(
154
  min_lat: float,
155
  max_lat: float,
156
  drive_folder_id_input: str,
 
157
  ):
158
  """
159
- Gradio handler:
160
- 1) parse points
161
- 2) make figure
162
- 3) upload to Drive (if configured)
163
- 4) return preview image and link
164
  """
165
- points = _parse_points(points_text)
166
- extent = (min_lon, max_lon, min_lat, max_lat)
167
- local_png = _make_figure(points, title, extent=extent)
 
 
 
168
 
169
- # Build Drive service from env
170
  service = _drive_service_from_env()
171
  if service is None:
172
- # Tell user how to set up secrets
173
  msg = (
174
- "⚠️ Google Drive upload is not configured. "
175
- "Please add a Hugging Face Space secret named `SERVICE_ACCOUNT_JSON` "
176
- "with your service account JSON. Optionally add `DRIVE_FOLDER_ID`."
177
  )
178
- # Return just the preview, no link
179
  return local_png, msg
180
 
181
- # Determine folder ID (priority: user input > env)
182
  folder_id_env = os.environ.get("DRIVE_FOLDER_ID", "").strip()
183
  folder_id = (drive_folder_id_input or folder_id_env or "").strip() or None
184
 
185
  try:
186
- file_id, view_link = _upload_to_drive(local_png, service, folder_id=folder_id)
187
- link_text = f"✅ Uploaded to Google Drive.\n• File ID: `{file_id}`\n• Open: {view_link}"
188
- return local_png, link_text
 
 
 
 
 
 
 
 
 
 
 
 
189
  except Exception as e:
190
  return local_png, f"❌ Upload failed: {e}"
191
 
@@ -195,10 +213,13 @@ with gr.Blocks(title="Map → Google Drive Uploader") as demo:
195
  gr.Markdown(
196
  """
197
  # 🗺️ Map → Google Drive Uploader
198
- Paste lon,lat pairs (one per line), set the extent, and click **Generate & Upload**.
199
 
200
- **Note:** To enable Google Drive upload in this Space, add the secret `SERVICE_ACCOUNT_JSON` (full service account JSON)
201
- and optionally `DRIVE_FOLDER_ID` in the Space settings.
 
 
 
202
  """
203
  )
204
 
@@ -219,9 +240,12 @@ and optionally `DRIVE_FOLDER_ID` in the Space settings.
219
  max_lat_in = gr.Number(label="max_lat", value=DEFAULT_EXTENT[3])
220
 
221
  folder_id_in = gr.Textbox(
222
- label="Google Drive Folder ID (optional — overrides Space secret)",
223
  placeholder="e.g. 1AbCdEfGhIJKlmnopQRstuVWxyz",
224
  )
 
 
 
225
 
226
  run_btn = gr.Button("🚀 Generate & Upload")
227
 
@@ -231,7 +255,16 @@ and optionally `DRIVE_FOLDER_ID` in the Space settings.
231
 
232
  run_btn.click(
233
  fn=generate_and_upload,
234
- inputs=[points_in, title_in, min_lon_in, max_lon_in, min_lat_in, max_lat_in, folder_id_in],
 
 
 
 
 
 
 
 
 
235
  outputs=[out_image, out_msg],
236
  )
237
 
 
1
  import os
2
  import io
3
  import time
4
+ import json
5
  import tempfile
6
  from typing import Optional, Tuple, List
7
 
8
  import matplotlib.pyplot as plt
9
  import gradio as gr
10
 
 
11
  from googleapiclient.discovery import build
12
  from googleapiclient.http import MediaIoBaseUpload
13
+ from googleapiclient.errors import HttpError
14
  from google.oauth2.service_account import Credentials
15
 
16
 
17
+ # ---------------- Config ----------------
18
  DEFAULT_EXTENT = (119.0, 123.5, 21.5, 25.5) # (min_lon, max_lon, min_lat, max_lat) ~ Taiwan region
19
+ SCOPES = ["https://www.googleapis.com/auth/drive.file"] # create & manage files created by the app
20
 
21
 
22
+ # --------------- Helpers ----------------
23
  def _parse_points(csv_text: str) -> List[Tuple[float, float]]:
24
  """
25
  Accepts lines like:
26
  121.5,25.0
27
  120.9,24.5
28
+ (lon,lat). Ignores blank/comment lines and invalid rows.
 
29
  """
30
+ pts: List[Tuple[float, float]] = []
31
  if not csv_text:
32
  return pts
33
  for ln in csv_text.strip().splitlines():
34
  ln = ln.strip()
35
  if not ln or ln.startswith("#"):
36
  continue
37
+ # allow comma or tab
38
  parts = [p.strip() for p in ln.replace("\t", ",").split(",")]
39
  if len(parts) < 2:
40
  continue
 
43
  lat = float(parts[1])
44
  pts.append((lon, lat))
45
  except ValueError:
 
46
  continue
47
  return pts
48
 
 
54
  dpi: int = 150,
55
  ) -> str:
56
  """
57
+ Draw a simple lon/lat scatter within the extent and save to PNG.
58
+ Returns local file path.
59
  """
60
  min_lon, max_lon, min_lat, max_lat = extent
61
  fig, ax = plt.subplots(figsize=(6.5, 6.5), dpi=dpi)
62
  ax.set_title(title or "Map Plot", pad=10)
63
  ax.set_xlabel("Longitude")
64
  ax.set_ylabel("Latitude")
 
 
65
  ax.set_xlim(min_lon, max_lon)
66
  ax.set_ylim(min_lat, max_lat)
67
  ax.grid(True, linestyle=":", linewidth=0.8)
68
 
69
+ # region frame
70
+ ax.add_patch(plt.Rectangle((min_lon, min_lat),
71
+ max_lon - min_lon,
72
+ max_lat - min_lat,
73
+ fill=False, linestyle="--", linewidth=1.0))
 
 
 
 
 
74
 
 
75
  if points:
76
  xs = [p[0] for p in points]
77
  ys = [p[1] for p in points]
78
  ax.scatter(xs, ys, s=60, marker="*", label="Input points")
79
  ax.legend(loc="upper right")
80
 
 
81
  tmpdir = tempfile.gettempdir()
82
  ts = int(time.time())
83
+ fname = f"map_{ts}.png"
84
+ out_path = os.path.join(tmpdir, fname)
85
  fig.tight_layout()
86
  fig.savefig(out_path)
87
  plt.close(fig)
 
90
 
91
  def _drive_service_from_env() -> Optional[object]:
92
  """
93
+ Build Google Drive service using Space secret `SERVICE_ACCOUNT_JSON`.
 
94
  """
95
  sa_json = os.environ.get("SERVICE_ACCOUNT_JSON", "").strip()
96
  if not sa_json:
97
  return None
 
 
 
 
98
  try:
99
  info = json.loads(sa_json)
100
+ creds = Credentials.from_service_account_info(info, scopes=SCOPES)
101
+ # cache_discovery=False avoids warning on server
102
+ service = build("drive", "v3", credentials=creds, cache_discovery=False)
103
+ return service
104
  except Exception:
105
  return None
106
 
 
 
 
 
107
 
108
  def _upload_to_drive(
109
  local_path: str,
110
  service,
111
  folder_id: Optional[str] = None,
112
+ make_public_link: bool = False,
113
  ) -> Tuple[str, str]:
114
  """
115
+ Upload file to Google Drive. Returns (file_id, webViewLink).
116
+ Supports Shared Drives. Optionally sets "anyone with the link: reader".
117
  """
118
  fname = os.path.basename(local_path)
119
  file_metadata = {"name": fname}
 
124
  media = MediaIoBaseUpload(f, mimetype="image/png", resumable=False)
125
  created = (
126
  service.files()
127
+ .create(
128
+ body=file_metadata,
129
+ media_body=media,
130
+ fields="id, webViewLink, webContentLink",
131
+ supportsAllDrives=True,
132
+ )
133
  .execute()
134
  )
135
 
136
  file_id = created.get("id", "")
137
+ view_link = created.get("webViewLink", "")
138
+
139
+ # (Optional) Make it publicly readable via link
140
+ if make_public_link and file_id:
141
+ try:
142
+ service.permissions().create(
143
+ fileId=file_id,
144
+ body={"role": "reader", "type": "anyone"},
145
+ fields="id",
146
+ supportsAllDrives=True,
147
+ ).execute()
148
+ except HttpError:
149
+ # ignore if permission change fails (e.g., domain policy)
150
+ pass
151
+
152
+ return file_id, view_link
153
 
154
 
155
+ # --------------- Gradio logic ---------------
156
  def generate_and_upload(
157
  points_text: str,
158
  title: str,
 
161
  min_lat: float,
162
  max_lat: float,
163
  drive_folder_id_input: str,
164
+ make_public: bool,
165
  ):
166
  """
167
+ 1) parse points
168
+ 2) render map & save PNG
169
+ 3) upload to Drive (if configured)
170
+ 4) return preview + link / diagnostics
 
171
  """
172
+ try:
173
+ points = _parse_points(points_text)
174
+ extent = (min_lon, max_lon, min_lat, max_lat)
175
+ local_png = _make_figure(points, title, extent=extent)
176
+ except Exception as e:
177
+ return None, f"❌ Failed to render figure: {e}"
178
 
 
179
  service = _drive_service_from_env()
180
  if service is None:
 
181
  msg = (
182
+ "⚠️ Google Drive upload is not configured.\n"
183
+ "請在 Space Settings Secrets 新增 `SERVICE_ACCOUNT_JSON`(貼完整 JSON)。\n"
184
+ "(可選)新增 `DRIVE_FOLDER_ID` 指定上傳資料夾。"
185
  )
 
186
  return local_png, msg
187
 
 
188
  folder_id_env = os.environ.get("DRIVE_FOLDER_ID", "").strip()
189
  folder_id = (drive_folder_id_input or folder_id_env or "").strip() or None
190
 
191
  try:
192
+ file_id, view_link = _upload_to_drive(
193
+ local_png, service, folder_id=folder_id, make_public_link=make_public
194
+ )
195
+ details = [
196
+ "✅ Uploaded to Google Drive.",
197
+ f"• File ID: `{file_id}`",
198
+ ]
199
+ if view_link:
200
+ details.append(f"• Open: {view_link}")
201
+ if not make_public:
202
+ details.append("• Note: 檔案遵循資料夾/雲端硬碟原有的分享權限(未自動公開)。")
203
+ return local_png, "\n".join(details)
204
+ except HttpError as e:
205
+ # Common causes: folder not shared to SA, insufficient permissions, shared drive policy
206
+ return local_png, f"❌ Upload failed (HttpError): {e}"
207
  except Exception as e:
208
  return local_png, f"❌ Upload failed: {e}"
209
 
 
213
  gr.Markdown(
214
  """
215
  # 🗺️ Map → Google Drive Uploader
216
+ 貼上 `lon,lat`(每行一組),設定視窗範圍並上傳至 Google Drive。
217
 
218
+ **設定提醒:**
219
+ - Space **Settings Secrets** 新增:
220
+ - `SERVICE_ACCOUNT_JSON`:貼完整的 Service Account JSON
221
+ - (可選)`DRIVE_FOLDER_ID`:Google Drive 資料夾 ID
222
+ - 若要上傳到特定資料夾,請先在 Google Drive 將該資料夾**分享給** Service Account 的 email(編輯者)。
223
  """
224
  )
225
 
 
240
  max_lat_in = gr.Number(label="max_lat", value=DEFAULT_EXTENT[3])
241
 
242
  folder_id_in = gr.Textbox(
243
+ label="Google Drive Folder ID(可選,會覆蓋 Space Secret)",
244
  placeholder="e.g. 1AbCdEfGhIJKlmnopQRstuVWxyz",
245
  )
246
+ make_public_in = gr.Checkbox(
247
+ label="建立公開連結(anyone with the link, reader)", value=False
248
+ )
249
 
250
  run_btn = gr.Button("🚀 Generate & Upload")
251
 
 
255
 
256
  run_btn.click(
257
  fn=generate_and_upload,
258
+ inputs=[
259
+ points_in,
260
+ title_in,
261
+ min_lon_in,
262
+ max_lon_in,
263
+ min_lat_in,
264
+ max_lat_in,
265
+ folder_id_in,
266
+ make_public_in,
267
+ ],
268
  outputs=[out_image, out_msg],
269
  )
270