MarcoXL commited on
Commit
ffe24e3
·
1 Parent(s): 7e7f1db

Upload folder using huggingface_hub

Browse files
Files changed (7) hide show
  1. README.md +4 -1
  2. app.py +36 -28
  3. graph.sqlite +3 -0
  4. requirements.txt +0 -2
  5. web/app.js +46 -5
  6. web/index.html +3 -0
  7. web/styles.css +13 -0
README.md CHANGED
@@ -26,7 +26,10 @@ license: mit
26
  <span style="color:#b8a2e0">紫=桥接边</span>,白色加粗虚线=高花费边。
27
  `B` 切换电子地图,`E` 切换沿路走线/直线。
28
  - **右栏**:选中节点或边的完整属性与自动判定信号。
 
 
 
29
 
30
  图片存在配套的私有 Dataset 里,由 CDN 按需分发。
31
 
32
- 建图规则见仓库的 `GRAPH_RULES.md`。
 
26
  <span style="color:#b8a2e0">紫=桥接边</span>,白色加粗虚线=高花费边。
27
  `B` 切换电子地图,`E` 切换沿路走线/直线。
28
  - **右栏**:选中节点或边的完整属性与自动判定信号。
29
+ - **街景朝向**:每个 pano 有 4 张街景,图下方一排按钮按罗盘顺时针列出
30
+ (`NE 40°`、`SE 130°`…),点一下换朝向。`H` 键下一个、`Shift`+`H` 上一个;
31
+ 选中一条边时两侧会同步翻,方便对同朝向比较。
32
 
33
  图片存在配套的私有 Dataset 里,由 CDN 按需分发。
34
 
35
+ 建图规则见 `GRAPH_RULES.md`(在源仓库里,**不随本 Space 一起上传**)。
app.py CHANGED
@@ -5,8 +5,9 @@
5
  三栏布局、J/K 键盘导航、底图切换、邻接矩阵。区别只有一个——**去掉了全部写入**:
6
  没有判定按钮、没有保存、没有新增边,后端也没有对应的接口。
7
 
8
- 架构:FastAPI 提供静态页面与只读 API,Gradio 挂在 /gradio(HF Spaces 的
9
- gradio SDK 需要一个可挂载的 Blocks,同时它也作为无 JS 时的兜底视图)。
 
10
 
11
  图片来源两种,按需切换:
12
  - 本地包:SETVL_PACKAGE 指向 prepare.py 的产出目录
@@ -23,7 +24,6 @@ import threading
23
  from pathlib import Path
24
  from typing import Any, Dict, List, Optional
25
 
26
- import gradio as gr
27
  from fastapi import FastAPI, HTTPException, Query
28
  from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
29
 
@@ -65,6 +65,22 @@ def con() -> sqlite3.Connection:
65
  return c
66
 
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  def asset_path(rel: str) -> Optional[Path]:
69
  """包内相对路径 -> 本地文件。数据集模式下按需从 CDN 拉取并缓存。"""
70
  if not rel or ".." in rel:
@@ -268,12 +284,23 @@ def scene(scene_id: str) -> JSONResponse:
268
  "SELECT * FROM edge WHERE scene_id=? ORDER BY source_label, target_label",
269
  (scene_id,))]
270
  effective(nodes, edges)
271
- # 街景路径:打包时建的索引,运行时猜不出文件名(含 alt/year/heading)
 
 
 
 
 
 
 
 
 
 
 
272
  for n in nodes:
273
- row = c.execute("SELECT rel FROM ground_index WHERE panoid=? LIMIT 1",
274
- (n["panoid"],)).fetchone()
275
- n["ground_asset"] = row[0] if row else None
276
- n["ground_assets"] = [row[0]] if row else []
277
  return JSONResponse({"scene": dict(s), "nodes": nodes, "edges": edges,
278
  "asset": {"satellite": dict(s)["satellite_path"]}})
279
 
@@ -292,26 +319,7 @@ def serve_asset(path: str = Query(...)) -> FileResponse:
292
  return FileResponse(p)
293
 
294
 
295
- # ---------------------------------------------------------------- Gradio 兜底
296
-
297
- def _gradio_fallback() -> gr.Blocks:
298
- st = None
299
- try:
300
- st = stats_payload()
301
- except Exception:
302
- pass
303
- with gr.Blocks(title="SetVL Node Graph Viewer") as demo:
304
- if st:
305
- gr.Markdown(
306
- f"# SetVL Node Graph Viewer\n\n"
307
- f"{st['scenes']:,} 场景 · {st['nodes']:,} 节点 · {st['edges']:,} 边\n\n"
308
- f"**[→ 打开浏览器界面](/)**")
309
- else:
310
- gr.Markdown("# SetVL Node Graph Viewer\n\n数据库未就绪。")
311
- return demo
312
-
313
-
314
- app = gr.mount_gradio_app(api, _gradio_fallback(), path="/gradio")
315
 
316
 
317
  if __name__ == "__main__":
 
5
  三栏布局、J/K 键盘导航、底图切换、邻接矩阵。区别只有一个——**去掉了全部写入**:
6
  没有判定按钮、没有保存、没有新增边,后端也没有对应的接口。
7
 
8
+ 架构:FastAPI 提供静态页面与只读 API,HF Spaces 用 docker SDK 跑。
9
+ 曾经在 /gradio 挂过一个 Gradio Blocks 当兜底视图,已移除——它只是一段
10
+ 带链接的 Markdown,却把 gradio 拖进了镜像依赖。
11
 
12
  图片来源两种,按需切换:
13
  - 本地包:SETVL_PACKAGE 指向 prepare.py 的产出目录
 
24
  from pathlib import Path
25
  from typing import Any, Dict, List, Optional
26
 
 
27
  from fastapi import FastAPI, HTTPException, Query
28
  from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
29
 
 
65
  return c
66
 
67
 
68
+ _HAS_HEADING: Optional[bool] = None
69
+
70
+
71
+ def ground_has_heading(c: sqlite3.Connection) -> bool:
72
+ """旧包的 ground_index 只有 (panoid, rel),没有 heading 列。
73
+
74
+ 重新 prepare 过的包才有。这里探一次并记住,免得每个节点都查一次
75
+ pragma;旧包降级成「只有一张街景」而不是 500。
76
+ """
77
+ global _HAS_HEADING
78
+ if _HAS_HEADING is None:
79
+ cols = {r[1] for r in c.execute("PRAGMA table_info(ground_index)")}
80
+ _HAS_HEADING = "heading" in cols
81
+ return _HAS_HEADING
82
+
83
+
84
  def asset_path(rel: str) -> Optional[Path]:
85
  """包内相对路径 -> 本地文件。数据集模式下按需从 CDN 拉取并缓存。"""
86
  if not rel or ".." in rel:
 
284
  "SELECT * FROM edge WHERE scene_id=? ORDER BY source_label, target_label",
285
  (scene_id,))]
286
  effective(nodes, edges)
287
+ # 街景路径:打包时建的索引,运行时猜不出文件名(含 alt/year/heading)。
288
+ # 每个 pano 有 4 个朝向,全部返回;按 heading 排序让前端的顺序稳定。
289
+ sql = ("SELECT rel, heading FROM ground_index WHERE panoid=?"
290
+ if ground_has_heading(c) else
291
+ "SELECT rel, NULL FROM ground_index WHERE panoid=?")
292
+
293
+ def order(row) -> tuple:
294
+ # 文件名里的 heading 有负角(-140 等),按原值排出来的是 SW/NW/NE/SE。
295
+ # 归一到 [0,360) 再排,朝向条就是正北起顺时针,和地图对得上。
296
+ h = row[1]
297
+ return (1, 0.0, row[0]) if h is None else (0, (float(h) % 360 + 360) % 360, row[0])
298
+
299
  for n in nodes:
300
+ rows = sorted(c.execute(sql, (n["panoid"],)).fetchall(), key=order)
301
+ n["ground_assets"] = [r[0] for r in rows]
302
+ n["ground_headings"] = [r[1] for r in rows]
303
+ n["ground_asset"] = rows[0][0] if rows else None
304
  return JSONResponse({"scene": dict(s), "nodes": nodes, "edges": edges,
305
  "asset": {"satellite": dict(s)["satellite_path"]}})
306
 
 
319
  return FileResponse(p)
320
 
321
 
322
+ app = api
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
 
325
  if __name__ == "__main__":
graph.sqlite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:58a07b075f692f69317cc1956f4eff308e6ca3c112d711c38cbb32f6b195f721
3
+ size 153534464
requirements.txt CHANGED
@@ -1,5 +1,3 @@
1
  fastapi>=0.115
2
  uvicorn[standard]>=0.30
3
- gradio>=5.0
4
- pillow>=10.0
5
  huggingface_hub>=0.25
 
1
  fastapi>=0.115
2
  uvicorn[standard]>=0.30
 
 
3
  huggingface_hub>=0.25
web/app.js CHANGED
@@ -513,22 +513,48 @@ function drawAdjacency() {
513
  });
514
  }
515
 
516
- function groundUrl(panoid) {
517
- // 街景文件名带朝向,这里只用节点坐标做占位;真实图由 /asset 白名单服务。
518
- return null;
 
 
 
 
 
 
519
  }
 
520
  function drawGroundImages() {
521
  var sel = S.selected;
522
  if (!sel || !S.scene) return;
523
  function fill(role, label) {
524
  var n = S.scene.nodes.find(function (x) { return x.label === label; });
525
  $(role + 'Badge').textContent = label || '—';
 
 
 
 
526
  $(role + 'Caption').textContent = n
527
- ? (n.lat.toFixed(6) + ', ' + n.lon.toFixed(6) + ' · ' + (n.corridor_name_auto || '无走廊'))
 
528
  : '—';
529
  var img = $(role + 'Image');
530
- if (n && n.ground_asset) img.src = '/asset?path=' + encodeURIComponent(n.ground_asset);
531
  else { img.removeAttribute('src'); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
  }
533
  if (sel.kind === 'node') {
534
  $('anchorKind').textContent = 'Node';
@@ -542,6 +568,20 @@ function drawGroundImages() {
542
  }
543
  }
544
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
  function pickNode(n) {
546
  S.selected = { kind: 'node', scene_id: S.scene.scene.scene_id,
547
  key: [S.scene.scene.scene_id, n.label] };
@@ -763,6 +803,7 @@ function boot() {
763
  else if (k === ']') { page(1); ev.preventDefault(); }
764
  else if (k === 'q') { setMode(S.mode === 'queue' ? 'scenes' : 'queue'); ev.preventDefault(); }
765
  else if (k === 'e') { setEdgeMode(S.edgeMode === 'road' ? 'straight' : 'road'); ev.preventDefault(); }
 
766
  });
767
  }
768
  document.addEventListener('DOMContentLoaded', boot);
 
513
  });
514
  }
515
 
516
+ // 每个 role(anchor/target)当前看的是第几个朝向。
517
+ // 换节点时保留同一个下标,这样连按 J/K 浏览时朝向不会跳来跳去。
518
+ var groundView = { anchor: 0, target: 0 };
519
+
520
+ function headingLabel(h) {
521
+ if (h == null) return '—';
522
+ var d = ((+h % 360) + 360) % 360; // 文件名里有负角,先归一到 [0,360)
523
+ var names = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
524
+ return names[Math.round(d / 45) % 8] + ' ' + Math.round(d) + '°';
525
  }
526
+
527
  function drawGroundImages() {
528
  var sel = S.selected;
529
  if (!sel || !S.scene) return;
530
  function fill(role, label) {
531
  var n = S.scene.nodes.find(function (x) { return x.label === label; });
532
  $(role + 'Badge').textContent = label || '—';
533
+ var assets = (n && n.ground_assets) || [];
534
+ var headings = (n && n.ground_headings) || [];
535
+ var i = Math.min(groundView[role] || 0, Math.max(assets.length - 1, 0));
536
+ groundView[role] = i;
537
  $(role + 'Caption').textContent = n
538
+ ? (n.lat.toFixed(6) + ', ' + n.lon.toFixed(6) + ' · ' + (n.corridor_name_auto || '无走廊')
539
+ + (assets.length > 1 ? ' · ' + (i + 1) + '/' + assets.length : ''))
540
  : '—';
541
  var img = $(role + 'Image');
542
+ if (assets[i]) img.src = '/asset?path=' + encodeURIComponent(assets[i]);
543
  else { img.removeAttribute('src'); }
544
+
545
+ var strip = $(role + 'Strip');
546
+ strip.innerHTML = '';
547
+ if (assets.length < 2) return; // 只有一张就不占一行
548
+ assets.forEach(function (rel, k) {
549
+ var b = document.createElement('button');
550
+ b.type = 'button';
551
+ b.textContent = headingLabel(headings[k]);
552
+ b.className = k === i ? 'active' : '';
553
+ b.setAttribute('role', 'tab');
554
+ b.setAttribute('aria-selected', k === i ? 'true' : 'false');
555
+ b.onclick = function () { groundView[role] = k; drawGroundImages(); };
556
+ strip.appendChild(b);
557
+ });
558
  }
559
  if (sel.kind === 'node') {
560
  $('anchorKind').textContent = 'Node';
 
568
  }
569
  }
570
 
571
+ // H 把当前可见的街景卡一起翻一个朝向——比较一对节点时两边保持同一朝向。
572
+ function cycleGround(step) {
573
+ if (!S.selected || !S.scene) return;
574
+ var labels = S.selected.kind === 'node'
575
+ ? [['anchor', S.selected.key[1]]]
576
+ : [['anchor', S.selected.key[1]], ['target', S.selected.key[2]]];
577
+ labels.forEach(function (pair) {
578
+ var n = S.scene.nodes.find(function (x) { return x.label === pair[1]; });
579
+ var len = ((n && n.ground_assets) || []).length;
580
+ if (len > 1) groundView[pair[0]] = ((groundView[pair[0]] + step) % len + len) % len;
581
+ });
582
+ drawGroundImages();
583
+ }
584
+
585
  function pickNode(n) {
586
  S.selected = { kind: 'node', scene_id: S.scene.scene.scene_id,
587
  key: [S.scene.scene.scene_id, n.label] };
 
803
  else if (k === ']') { page(1); ev.preventDefault(); }
804
  else if (k === 'q') { setMode(S.mode === 'queue' ? 'scenes' : 'queue'); ev.preventDefault(); }
805
  else if (k === 'e') { setEdgeMode(S.edgeMode === 'road' ? 'straight' : 'road'); ev.preventDefault(); }
806
+ else if (k === 'h') { cycleGround(ev.shiftKey ? -1 : 1); ev.preventDefault(); }
807
  });
808
  }
809
  document.addEventListener('DOMContentLoaded', boot);
web/index.html CHANGED
@@ -195,6 +195,7 @@
195
  <button type="button" class="image-stage ground-stage" data-role="anchor" aria-label="放大街景">
196
  <img id="anchorImage" alt="ground view">
197
  </button>
 
198
  <div class="image-caption mono" id="anchorCaption">—</div>
199
  </article>
200
  <article class="image-card ground-card" id="targetCard">
@@ -205,6 +206,7 @@
205
  <button type="button" class="image-stage ground-stage" data-role="target" aria-label="放大街景">
206
  <img id="targetImage" alt="ground view">
207
  </button>
 
208
  <div class="image-caption mono" id="targetCaption">—</div>
209
  </article>
210
  </div>
@@ -263,6 +265,7 @@
263
  <kbd>K</kbd><span>队列上一条</span>
264
  <kbd>B</kbd><span>切换卫星图 / 电子地图</span>
265
  <kbd>E</kbd><span>切换边的画法:沿路走线 / 直线</span>
 
266
  <kbd>[</kbd> <kbd>]</kbd><span>列表上一页 / 下一页</span>
267
  <kbd>Q</kbd><span>切换「全部场景」/「带信号的」</span>
268
  <kbd>?</kbd><span>打开本帮助</span>
 
195
  <button type="button" class="image-stage ground-stage" data-role="anchor" aria-label="放大街景">
196
  <img id="anchorImage" alt="ground view">
197
  </button>
198
+ <div class="heading-strip" id="anchorStrip" role="tablist" aria-label="街景朝向"></div>
199
  <div class="image-caption mono" id="anchorCaption">—</div>
200
  </article>
201
  <article class="image-card ground-card" id="targetCard">
 
206
  <button type="button" class="image-stage ground-stage" data-role="target" aria-label="放大街景">
207
  <img id="targetImage" alt="ground view">
208
  </button>
209
+ <div class="heading-strip" id="targetStrip" role="tablist" aria-label="街景朝向"></div>
210
  <div class="image-caption mono" id="targetCaption">—</div>
211
  </article>
212
  </div>
 
265
  <kbd>K</kbd><span>队列上一条</span>
266
  <kbd>B</kbd><span>切换卫星图 / 电子地图</span>
267
  <kbd>E</kbd><span>切换边的画法:沿路走线 / 直线</span>
268
+ <kbd>H</kbd><span>下一个街景朝向(<kbd>Shift</kbd>+<kbd>H</kbd> 上一个)</span>
269
  <kbd>[</kbd> <kbd>]</kbd><span>列表上一页 / 下一页</span>
270
  <kbd>Q</kbd><span>切换「全部场景」/「带信号的」</span>
271
  <kbd>?</kbd><span>打开本帮助</span>
web/styles.css CHANGED
@@ -231,6 +231,19 @@ kbd {
231
  .map-controls { display: flex; border: 1px solid var(--line); }
232
  .map-controls button { background: transparent; border: 0; width: 22px; cursor: pointer; color: var(--muted); font: 700 12px var(--mono); }
233
  .map-controls button:hover { color: var(--signal); }
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  .image-caption { min-height: 34px; padding: 8px 11px; display: flex; justify-content: space-between; gap: 10px; color: var(--faint); border-top: 1px solid var(--line-soft); font: 600 9px var(--mono); }
235
  .image-caption.mono { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
236
 
 
231
  .map-controls { display: flex; border: 1px solid var(--line); }
232
  .map-controls button { background: transparent; border: 0; width: 22px; cursor: pointer; color: var(--muted); font: 700 12px var(--mono); }
233
  .map-controls button:hover { color: var(--signal); }
234
+ /* 街景朝向切换条:一个 pano 有 4 个朝向,全部列出来可点 */
235
+ .heading-strip {
236
+ display: flex; gap: 1px; border-top: 1px solid var(--line-soft);
237
+ background: var(--line-soft);
238
+ }
239
+ .heading-strip:empty { display: none; }
240
+ .heading-strip button {
241
+ flex: 1; background: var(--panel); border: 0; padding: 4px 0; cursor: pointer;
242
+ color: var(--faint); font: 700 9px var(--mono); letter-spacing: .06em;
243
+ }
244
+ .heading-strip button:hover { color: var(--cyan); }
245
+ .heading-strip button.active { background: var(--signal); color: var(--ink); }
246
+
247
  .image-caption { min-height: 34px; padding: 8px 11px; display: flex; justify-content: space-between; gap: 10px; color: var(--faint); border-top: 1px solid var(--line-soft); font: 600 9px var(--mono); }
248
  .image-caption.mono { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
249