MarcoXL commited on
Commit
b866a78
·
1 Parent(s): 8427646

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. .gitattributes +1 -0
  2. README.md +15 -5
  3. app.py +403 -0
  4. graph.sqlite-shm +0 -0
  5. graph.sqlite-wal +0 -0
  6. requirements.txt +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ graph.sqlite filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,23 @@
1
  ---
2
- title: Setvl Node Graph Viewer
3
- emoji: 🏢
4
  colorFrom: gray
5
- colorTo: pink
6
  sdk: gradio
7
  sdk_version: 6.27.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: SetVL Node Graph Viewer
3
+ emoji: 🛣️
4
  colorFrom: gray
5
+ colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.27.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
  ---
12
 
13
+ # SetVL Node Graph Viewer
14
+
15
+ 只读浏览 SetVL-480K 全库的 node graph:12,250 个场景、119,996 个节点、108,374 条边。
16
+
17
+ 节点按所属道路走廊着色,红圈为孤儿节点;边按来源着色——青=同路相连、
18
+ 橙=路口相连、紫=最小补全的桥接边,白色加粗=高花费边(路网上绕路过远或不可达,
19
+ 按直线连接)。
20
+
21
+ 图片存放在配套的 Dataset repo,由 CDN 按需分发。
22
+
23
+ 建图规则见仓库里的 `GRAPH_RULES.md`。
app.py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """SetVL Node Graph 只读浏览器(Gradio)。
3
+
4
+ 给别人看结果用的:能翻场景、看图、看每条边每个节点的属性,但**改不了任何东西**。
5
+ 没有写入接口——不是靠界面藏起来,是压根没实现。
6
+
7
+ 数据来自 `prepare.py` 打出来的包(graph.sqlite + satellite/ + ground/ 全是包内相对路径),
8
+ 所以整个目录拷到哪都能跑,不依赖原始的 52 GB 数据集。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import math
16
+ import os
17
+ import sqlite3
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Optional, Tuple
20
+
21
+ import gradio as gr
22
+ from PIL import Image, ImageDraw
23
+
24
+ HERE = Path(__file__).resolve().parent
25
+
26
+ # 图片从哪来,两种模式:
27
+ # 本地包 —— SETVL_PACKAGE 指向 prepare.py 产出的目录,直接读文件
28
+ # HF 数据集 —— SETVL_DATASET 指向 "user/repo",按需从 CDN 取,本地只留缓存
29
+ # 27 GB 图片放 Dataset repo、Space 里只放 93 MB 的数据库,Space 构建才快。
30
+ PACKAGE = Path(os.environ.get("SETVL_PACKAGE", HERE / "package"))
31
+ DATASET = os.environ.get("SETVL_DATASET", "").strip()
32
+ DB = Path(os.environ.get("SETVL_DB", "")) if os.environ.get("SETVL_DB") else None
33
+ if DB is None:
34
+ DB = (HERE / "graph.sqlite") if (HERE / "graph.sqlite").exists() else PACKAGE / "graph.sqlite"
35
+
36
+ ZOOM, RASTER = 17, 400
37
+ CITIES: List[str] = []
38
+
39
+ # 与复核台一致的配色,换个地方看图不会认错
40
+ COL = {
41
+ "corridor_chain": (104, 200, 192),
42
+ "intersection": (241, 164, 60),
43
+ "bridge": (184, 162, 224),
44
+ "manual": (104, 200, 192),
45
+ }
46
+ COL_ORPHAN = (239, 91, 77)
47
+ COL_HIGHCOST = (232, 229, 218)
48
+
49
+
50
+ def asset(rel: str) -> Optional[Path]:
51
+ """把包内相对路径解析成本地可读文件。
52
+
53
+ 数据集模式下按需从 HF CDN 拉取并缓存——27 GB 不必也不该整个下到 Space 里。
54
+ 取不到就返回 None,让调用方降级(画空白底图、跳过该张街景),不要抛异常
55
+ 把整个页面打挂。
56
+ """
57
+ local = PACKAGE / rel
58
+ if local.is_file():
59
+ return local
60
+ if not DATASET:
61
+ return None
62
+ try:
63
+ from huggingface_hub import hf_hub_download
64
+ return Path(hf_hub_download(DATASET, rel, repo_type="dataset"))
65
+ except Exception:
66
+ return None
67
+
68
+
69
+ def connect() -> sqlite3.Connection:
70
+ if not DB.exists():
71
+ raise FileNotFoundError(
72
+ f"找不到 {DB}。先跑 prepare.py 打包,或设 SETVL_DB 指向 graph.sqlite。")
73
+ con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True, check_same_thread=False)
74
+ con.row_factory = sqlite3.Row
75
+ return con
76
+
77
+
78
+ CON = None
79
+
80
+
81
+ def con() -> sqlite3.Connection:
82
+ global CON
83
+ if CON is None:
84
+ CON = connect()
85
+ return CON
86
+
87
+
88
+ # --------------------------------------------------------------- 投影
89
+
90
+ def mercator(lat: float, lon: float) -> Tuple[float, float]:
91
+ s = 256 * (2 ** ZOOM)
92
+ x = (lon + 180.0) / 360.0 * s
93
+ sy = math.sin(math.radians(lat))
94
+ y = (0.5 - math.log((1 + sy) / (1 - sy)) / (4 * math.pi)) * s
95
+ return x, y
96
+
97
+
98
+ def tile_center(sat_rel: str) -> Optional[Tuple[float, float]]:
99
+ parts = Path(sat_rel).stem.split("_")
100
+ try:
101
+ return float(parts[-1]), float(parts[-2])
102
+ except (ValueError, IndexError):
103
+ return None
104
+
105
+
106
+ def projector(sat_rel: str):
107
+ c = tile_center(sat_rel)
108
+ if c is None:
109
+ return None
110
+ cx, cy = mercator(*c)
111
+
112
+ def to_px(lat: float, lon: float) -> Tuple[float, float]:
113
+ x, y = mercator(lat, lon)
114
+ return x - cx + RASTER / 2, y - cy + RASTER / 2
115
+ return to_px
116
+
117
+
118
+ def corridor_color(cid: Optional[str]) -> Tuple[int, int, int]:
119
+ if not cid:
120
+ return (120, 130, 125)
121
+ h = 0
122
+ for ch in cid:
123
+ h = (h * 31 + ord(ch)) & 0xFFFFFFFF
124
+ import colorsys
125
+ r, g, b = colorsys.hls_to_rgb((h % 360) / 360.0, 0.58, 0.55)
126
+ return int(r * 255), int(g * 255), int(b * 255)
127
+
128
+
129
+ # --------------------------------------------------------------- 查询
130
+
131
+ def list_scenes(city: str, min_nodes: int, only_high_cost: bool,
132
+ only_orphan: bool, query: str, page: int,
133
+ per_page: int = 60) -> Tuple[List[str], int]:
134
+ where, args = [], []
135
+ if city and city != "全部":
136
+ where.append("s.city = ?")
137
+ args.append(city)
138
+ if min_nodes:
139
+ where.append("s.node_count >= ?")
140
+ args.append(int(min_nodes))
141
+ if query.strip():
142
+ where.append("s.scene_id LIKE ?")
143
+ args.append(f"%{query.strip()}%")
144
+ if only_high_cost:
145
+ where.append("EXISTS (SELECT 1 FROM edge e WHERE e.scene_id = s.scene_id "
146
+ "AND e.high_cost_auto = 1)")
147
+ if only_orphan:
148
+ where.append("EXISTS (SELECT 1 FROM node n WHERE n.scene_id = s.scene_id "
149
+ "AND n.is_orphan_auto = 1)")
150
+ clause = (" WHERE " + " AND ".join(where)) if where else ""
151
+ total = con().execute(f"SELECT COUNT(*) FROM scene s{clause}", args).fetchone()[0]
152
+ page = max(0, page)
153
+ rows = con().execute(
154
+ f"SELECT s.scene_id, s.city, s.node_count FROM scene s{clause} "
155
+ "ORDER BY s.scene_id LIMIT ? OFFSET ?", args + [per_page, page * per_page])
156
+ labels = [f"{r['scene_id']} · {r['node_count']}n · {r['city']}" for r in rows]
157
+ return labels, total
158
+
159
+
160
+ def scene_of(label: str) -> Optional[str]:
161
+ return label.split(" · ")[0] if label else None
162
+
163
+
164
+ def load_scene(scene_id: str) -> Optional[Dict[str, Any]]:
165
+ s = con().execute("SELECT * FROM scene WHERE scene_id = ?", (scene_id,)).fetchone()
166
+ if s is None:
167
+ return None
168
+ nodes = [dict(r) for r in con().execute(
169
+ "SELECT * FROM node WHERE scene_id = ? ORDER BY label", (scene_id,))]
170
+ edges = [dict(r) for r in con().execute(
171
+ "SELECT * FROM edge WHERE scene_id = ? ORDER BY source_label, target_label",
172
+ (scene_id,))]
173
+ for n in nodes:
174
+ n["corridor_id"] = n["corridor_id_human"] or n["corridor_id_auto"]
175
+ n["corridor_name"] = (n["corridor_name_human"] or n["corridor_name_auto"]
176
+ or n["corridor_id"])
177
+ o = n["is_orphan_human"]
178
+ n["is_orphan"] = int(o) if o is not None else (n["is_orphan_auto"] or 0)
179
+ for e in edges:
180
+ e["origin"] = e["origin_human"] or e["origin_auto"] or "bridge"
181
+ hc = e["high_cost_human"]
182
+ e["high_cost"] = int(hc) if hc is not None else (e["high_cost_auto"] or 0)
183
+ e["in_graph"] = e["edge_verdict_human"] != "reject"
184
+ return {"scene": dict(s), "nodes": nodes, "edges": edges}
185
+
186
+
187
+ # --------------------------------------------------------------- 绘图
188
+
189
+ def render(scene_id: str, show_road_path: bool) -> Tuple[Optional[Image.Image], str, str]:
190
+ det = load_scene(scene_id)
191
+ if det is None:
192
+ return None, "未找到该场景", ""
193
+ sat = asset(det["scene"]["satellite_path"])
194
+ base = (Image.open(sat).convert("RGB") if sat
195
+ else Image.new("RGB", (RASTER, RASTER), (18, 22, 20)))
196
+ base = base.resize((RASTER * 2, RASTER * 2), Image.LANCZOS)
197
+ d = ImageDraw.Draw(base, "RGBA")
198
+ proj = projector(det["scene"]["satellite_path"])
199
+ if proj is None:
200
+ return base, "无法从瓦片名解析中心坐标", ""
201
+ P = {n["label"]: tuple(v * 2 for v in proj(n["lat"], n["lon"])) for n in det["nodes"]}
202
+
203
+ for e in det["edges"]:
204
+ a, b = P.get(e["source_label"]), P.get(e["target_label"])
205
+ if not a or not b:
206
+ continue
207
+ col = COL.get(e["origin"], COL["bridge"])
208
+ alpha = 60 if not e["in_graph"] else 255
209
+ if e["high_cost"]:
210
+ col = COL_HIGHCOST
211
+ path = None
212
+ if show_road_path and not e["high_cost"] and e["road_path_auto"]:
213
+ try:
214
+ pts = json.loads(e["road_path_auto"])
215
+ path = [tuple(v * 2 for v in proj(p[0], p[1])) for p in pts]
216
+ except (ValueError, TypeError):
217
+ path = None
218
+ if path and len(path) > 1:
219
+ d.line([a] + path + [b], fill=col + (alpha,), width=3, joint="curve")
220
+ else:
221
+ d.line([a, b], fill=col + (alpha,), width=3)
222
+
223
+ for n in det["nodes"]:
224
+ p = P[n["label"]]
225
+ fill = corridor_color(n["corridor_id"])
226
+ r = 9
227
+ d.ellipse([p[0] - r, p[1] - r, p[0] + r, p[1] + r], fill=fill + (255,),
228
+ outline=(COL_ORPHAN if n["is_orphan"] else (12, 16, 14)) + (255,),
229
+ width=3 if n["is_orphan"] else 2)
230
+ d.text((p[0] - 4, p[1] - 6), n["label"], fill=(12, 16, 14))
231
+
232
+ sc = det["scene"]
233
+ from collections import Counter
234
+ oc = Counter(e["origin"] for e in det["edges"])
235
+ corridors = sorted({n["corridor_name"] for n in det["nodes"] if n["corridor_name"]})
236
+ summary = "\n".join([
237
+ f"**{sc['scene_id']}**",
238
+ "",
239
+ f"- 城市:{sc['city']} 节点 {len(det['nodes'])} 边 {len(det['edges'])}",
240
+ f"- 边构成:走廊链 {oc.get('corridor_chain',0)} 路口 {oc.get('intersection',0)} "
241
+ f"桥接 {oc.get('bridge',0)} 人工 {oc.get('manual',0)}",
242
+ f"- 孤儿节点 {sum(n['is_orphan'] for n in det['nodes'])} "
243
+ f"高花费边 {sum(e['high_cost'] for e in det['edges'])} "
244
+ f"被拒边 {sum(0 if e['in_graph'] else 1 for e in det['edges'])}",
245
+ f"- 路网碎片化:{'是' if sc['net_fragmented_auto'] else '否'}",
246
+ "",
247
+ f"**走廊({len(corridors)})**:" + ("、".join(corridors[:8]) or "—")
248
+ + ("…" if len(corridors) > 8 else ""),
249
+ ])
250
+
251
+ lines = ["| 边 | 来源 | 欧氏 m | 路网 m | 绕路 | 高花费 | 状态 |",
252
+ "|---|---|---|---|---|---|---|"]
253
+ for e in det["edges"]:
254
+ lines.append(
255
+ f"| {e['source_label']}–{e['target_label']} | {e['origin']} | "
256
+ f"{(e['euclid_distance_m_auto'] or 0):.1f} | "
257
+ f"{('%.1f' % e['road_distance_m_auto']) if e['road_distance_m_auto'] else '—'} | "
258
+ f"{('%.2fx' % e['detour_ratio_auto']) if e['detour_ratio_auto'] else '—'} | "
259
+ f"{'是' if e['high_cost'] else ''} | "
260
+ f"{'已拒' if not e['in_graph'] else ''} |")
261
+ return base, summary, "\n".join(lines)
262
+
263
+
264
+ def node_gallery(scene_id: str) -> List[Tuple[str, str]]:
265
+ det = load_scene(scene_id)
266
+ if det is None:
267
+ return []
268
+ out = []
269
+ city = det["scene"]["city"]
270
+ folder = PACKAGE / "ground" / city
271
+ for n in det["nodes"]:
272
+ cap = (f"{n['label']} · {n['corridor_name'] or '无走廊'}"
273
+ + (" · 孤儿" if n["is_orphan"] else ""))
274
+ # 街景文件名带朝向,本地包直接 glob;数据集模式下朝向未知,
275
+ # 逐个试常见朝向取第一张命中的
276
+ if folder.is_dir():
277
+ hits = sorted(folder.glob(f"{city},{n['panoid']},*.webp"))
278
+ if hits:
279
+ out.append((str(hits[0]), cap))
280
+ continue
281
+ rel = ground_rel(n)
282
+ if rel:
283
+ p = asset(rel)
284
+ if p:
285
+ out.append((str(p), cap))
286
+ return out
287
+
288
+
289
+ def ground_rel(node: Dict[str, Any]) -> Optional[str]:
290
+ """从索引表查这个 pano 的街景文件名。
291
+
292
+ 打包时写了一张 pano -> 文件名 的索引,因为文件名里有 alt/year/heading
293
+ 三个字段是数据库里没有的,猜不出来。
294
+ """
295
+ row = con().execute(
296
+ "SELECT rel FROM ground_index WHERE panoid = ? LIMIT 1",
297
+ (node["panoid"],)).fetchone()
298
+ return row[0] if row else None
299
+
300
+
301
+ def node_table(scene_id: str) -> str:
302
+ det = load_scene(scene_id)
303
+ if det is None:
304
+ return ""
305
+ lines = ["| 节点 | 走廊 | 来源 | 吸附 m | 孤儿 | 人工覆盖 |", "|---|---|---|---|---|---|"]
306
+ for n in det["nodes"]:
307
+ ov = []
308
+ if n["corridor_id_human"] is not None:
309
+ ov.append("走廊")
310
+ if n["is_orphan_human"] is not None:
311
+ ov.append("孤儿")
312
+ lines.append(
313
+ f"| {n['label']} | {n['corridor_name'] or '—'} | "
314
+ f"{n['corridor_source_auto'] or '—'} | "
315
+ f"{(n['snap_distance_m_auto'] or 0):.1f} | {'是' if n['is_orphan'] else ''} | "
316
+ f"{'、'.join(ov)} |")
317
+ return "\n".join(lines)
318
+
319
+
320
+ # --------------------------------------------------------------- 界面
321
+
322
+ def build() -> gr.Blocks:
323
+ global CITIES
324
+ CITIES = ["全部"] + [r[0] for r in
325
+ con().execute("SELECT DISTINCT city FROM scene ORDER BY city")]
326
+ n_scene = con().execute("SELECT COUNT(*) FROM scene").fetchone()[0]
327
+ n_node = con().execute("SELECT COUNT(*) FROM node").fetchone()[0]
328
+ n_edge = con().execute("SELECT COUNT(*) FROM edge").fetchone()[0]
329
+
330
+ with gr.Blocks(title="SetVL Node Graph Viewer", theme=gr.themes.Base()) as demo:
331
+ gr.Markdown(
332
+ f"# SetVL Node Graph Viewer\n"
333
+ f"{n_scene:,} 个场景 · {n_node:,} 个节点 · {n_edge:,} 条边 · **只读浏览**\n\n"
334
+ f"节点按所属走廊着色,红圈是孤儿;边按来源着色:"
335
+ f"<span style='color:#68c8c0'>青=走廊链</span>、"
336
+ f"<span style='color:#f1a43c'>橙=路口边</span>、"
337
+ f"<span style='color:#b8a2e0'>紫=桥接边</span>,"
338
+ f"白色加粗=高花费边(直线连接)。")
339
+ with gr.Row():
340
+ with gr.Column(scale=1):
341
+ city = gr.Dropdown(CITIES, value="全部", label="城市")
342
+ query = gr.Textbox(label="scene_id 搜索", placeholder="片段即可")
343
+ min_nodes = gr.Slider(1, 38, value=1, step=1, label="最少节点数")
344
+ with gr.Row():
345
+ f_cost = gr.Checkbox(label="含高花费边")
346
+ f_orph = gr.Checkbox(label="含孤儿")
347
+ page = gr.Number(value=0, precision=0, label="页码(每页 60)")
348
+ info = gr.Markdown()
349
+ picker = gr.Radio(choices=[], label="场景", interactive=True)
350
+ with gr.Column(scale=2):
351
+ show_path = gr.Checkbox(value=True, label="按道路走线绘制边")
352
+ canvas = gr.Image(label="场景图", type="pil", height=560)
353
+ summary = gr.Markdown()
354
+ with gr.Row():
355
+ with gr.Column():
356
+ gr.Markdown("### 节点")
357
+ ntab = gr.Markdown()
358
+ with gr.Column():
359
+ gr.Markdown("### 边")
360
+ etab = gr.Markdown()
361
+ gallery = gr.Gallery(label="街景", columns=6, height=260)
362
+
363
+ def refresh(c, mn, hc, orp, q, pg):
364
+ labels, total = list_scenes(c, mn, hc, orp, q, int(pg or 0))
365
+ pages = max(1, -(-total // 60))
366
+ msg = f"命中 **{total:,}** 个场景 第 {int(pg or 0)+1} / {pages} 页"
367
+ return gr.update(choices=labels, value=labels[0] if labels else None), msg
368
+
369
+ def show(label, sp):
370
+ sid = scene_of(label)
371
+ if not sid:
372
+ return None, "", "", "", []
373
+ img, summ, et = render(sid, sp)
374
+ return img, summ, node_table(sid), et, node_gallery(sid)
375
+
376
+ for ctl in (city, min_nodes, f_cost, f_orph, query, page):
377
+ ctl.change(refresh, [city, min_nodes, f_cost, f_orph, query, page],
378
+ [picker, info])
379
+ picker.change(show, [picker, show_path], [canvas, summary, ntab, etab, gallery])
380
+ show_path.change(show, [picker, show_path], [canvas, summary, ntab, etab, gallery])
381
+ demo.load(refresh, [city, min_nodes, f_cost, f_orph, query, page], [picker, info])
382
+ return demo
383
+
384
+
385
+ def main() -> int:
386
+ ap = argparse.ArgumentParser(description=__doc__)
387
+ ap.add_argument("--package", type=Path)
388
+ ap.add_argument("--share", action="store_true", help="开公网临时链接(72 小时)")
389
+ ap.add_argument("--port", type=int, default=7860)
390
+ args = ap.parse_args()
391
+ if args.package:
392
+ global PACKAGE, DB
393
+ PACKAGE = args.package
394
+ DB = PACKAGE / "graph.sqlite"
395
+ import tempfile
396
+ build().launch(share=args.share, server_port=args.port,
397
+ allowed_paths=[str(PACKAGE), tempfile.gettempdir(),
398
+ os.path.expanduser("~/.cache/huggingface")])
399
+ return 0
400
+
401
+
402
+ if __name__ == "__main__":
403
+ raise SystemExit(main())
graph.sqlite-shm ADDED
Binary file (32.8 kB). View file
 
graph.sqlite-wal ADDED
File without changes
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=5.0
2
+ pillow>=10.0
3
+ huggingface_hub>=0.25