vivekchakraverty Claude Opus 4.8 commited on
Commit
842fed4
·
1 Parent(s): ffdac8f

Add home-tunnel proxy control panel (tools/)

Browse files

Tkinter GUI to run a local authenticated HTTP proxy on the user's residential IP, expose
it via a raw TCP tunnel (ngrok / bore / manual), test the egress IP + YouTube reach, and
one-click set the Space's YT_PROXY secret. Includes a step-by-step README. (Forward
proxies need a TCP tunnel; HTTP reverse tunnels like Cloudflare/Tailscale Funnel won't
carry CONNECT.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

tools/README.md ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Home Proxy Panel — give the Space your home IP
2
+
3
+ The TutorialMaker Space can't fetch transcripts or stream URLs from its datacenter IP
4
+ (YouTube blocks it). This tool runs a small **authenticated HTTP proxy on your computer**
5
+ (your home/residential IP) and exposes it through a **raw TCP tunnel**, then sets it as the
6
+ Space's **`YT_PROXY`** secret. The Space's YouTube requests then exit from *your* IP and
7
+ stop getting blocked.
8
+
9
+ > **Why a TCP tunnel (not Cloudflare/Tailscale):** a forward proxy speaks HTTP `CONNECT`,
10
+ > which only survives a transparent **TCP** tunnel. HTTP *reverse* tunnels (Cloudflare
11
+ > quick-tunnel, Tailscale Funnel) cannot carry it — use **ngrok TCP** or **bore**.
12
+
13
+ ---
14
+
15
+ ## 1. Install the Python deps
16
+
17
+ ```bash
18
+ pip install -r tools/requirements.txt
19
+ ```
20
+
21
+ ## 2. Install a tunnel (pick ONE)
22
+
23
+ **Option A — ngrok (more reliable, free account):**
24
+ 1. Sign up at <https://ngrok.com> (free).
25
+ 2. Install the ngrok agent and put `ngrok` on your PATH
26
+ (<https://ngrok.com/download>).
27
+ 3. Copy your authtoken from the ngrok dashboard → you'll paste it into the panel.
28
+
29
+ **Option B — bore (no account, less reliable):**
30
+ 1. Download `bore.exe` from <https://github.com/ekzhang/bore/releases> and put it on your
31
+ PATH (or in this folder).
32
+ 2. That's it — it uses the shared public `bore.pub` server.
33
+
34
+ ## 3. Launch the panel
35
+
36
+ ```bash
37
+ python tools/home_proxy_panel.py
38
+ ```
39
+
40
+ ## 4. Use it (in order)
41
+
42
+ 1. **Proxy port / user / password** are pre-filled (a random password is generated). Keep
43
+ the password — it protects your public proxy from being used by strangers.
44
+ 2. Pick the **Tunnel provider** (`ngrok`, `bore`, or `manual`). For ngrok, paste your
45
+ **authtoken**.
46
+ 3. Click **Start proxy** → it runs `proxy.py` on `127.0.0.1:<port>`.
47
+ 4. Click **Start tunnel** → the **Public endpoint** field fills in
48
+ (`x.tcp.ngrok.io:NNNN` or `bore.pub:NNNN`) and the **YT_PROXY URL** is computed.
49
+ 5. Click **Test proxy** → in the log, the **egress IP should be your home IP** and YouTube
50
+ should be **reachable**. If so, the proxy works.
51
+ 6. Click **Set YT_PROXY on Space** → it writes the `YT_PROXY` secret to your Space
52
+ (auto-detecting your HF token, or paste one). The Space restarts and picks it up.
53
+ - Or click **Copy YT_PROXY URL** and paste it into the Space's secret manually.
54
+
55
+ ## 5. Verify on the Space
56
+
57
+ Run a topic on the Space. The **transcript** and **screenshot** steps should now succeed
58
+ instead of failing with an SSL/TLS block.
59
+
60
+ ## Keep it running
61
+
62
+ The proxy + tunnel must stay running on your machine while the Space is in use. Close the
63
+ panel (or **Stop proxy/Stop tunnel**) when you're done — the `YT_PROXY` secret will then
64
+ point at a dead endpoint until you start it again (the Space falls back to text-only / a
65
+ clear error).
66
+
67
+ ---
68
+
69
+ ## Security notes
70
+
71
+ - The proxy is **public** while tunneled, so it **requires the username/password** — keep
72
+ them non-trivial (use **New password** to rotate). Without the right credentials nobody
73
+ can use your proxy.
74
+ - This routes YouTube traffic through your home connection and consumes your bandwidth.
75
+ - Stop the tunnel when not needed; rotate the password periodically.
76
+
77
+ ## Manual endpoint (router port-forward / other tunnel)
78
+
79
+ If you forward a router port (and you're **not** behind CGNAT) or use another raw-TCP
80
+ tunnel, choose provider **`manual`** and enter the public `host:port`. The panel will
81
+ build the `YT_PROXY` URL and can set the secret + test it.
82
+
83
+ ## Troubleshooting
84
+
85
+ - **`proxy.py` not found** → `pip install proxy.py`.
86
+ - **ngrok: no tcp tunnel** → ensure the authtoken is set; free TCP tunnels are supported
87
+ but only one at a time — close other ngrok agents.
88
+ - **Test egress IP is *not* your home IP** → the tunnel/proxy chain is wrong; restart both.
89
+ - **YouTube still blocked on the Space** → confirm the secret value matches the panel's
90
+ YT_PROXY URL exactly (including `user:pass@`), and that the panel is still running.
tools/home_proxy_panel.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Home-tunnel proxy control panel for the TutorialMaker Space.
2
+
3
+ Runs a local authenticated HTTP forward proxy on your machine (your residential IP) and
4
+ exposes it to the internet through a RAW TCP tunnel (ngrok / bore), so the Hugging Face
5
+ Space can route youtube-transcript-api + yt-dlp through your home IP and dodge the
6
+ datacenter-IP block. One-click sets the resulting URL as the Space's YT_PROXY secret.
7
+
8
+ Why a TCP tunnel: a forward proxy speaks HTTP CONNECT, which only survives a transparent
9
+ TCP tunnel. HTTP reverse tunnels (Cloudflare quick-tunnel, Tailscale Funnel) do NOT work
10
+ for this — don't use them here.
11
+
12
+ Run: python tools/home_proxy_panel.py
13
+ Deps: pip install proxy.py huggingface_hub requests (ngrok or bore on PATH for tunneling)
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import queue
20
+ import re
21
+ import secrets
22
+ import subprocess
23
+ import sys
24
+ import threading
25
+
26
+ CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".tutorialmaker_proxy.json")
27
+ DEFAULT_REPO = "vivekchakraverty/TutorialMaker"
28
+ _NO_WINDOW = 0x08000000 if os.name == "nt" else 0
29
+
30
+
31
+ # ----------------------------------------------------------------- pure helpers (tested)
32
+ def build_proxy_url(host: str, port, user: str, pw: str) -> str:
33
+ """Compose an http proxy URL, url-encoding the credentials."""
34
+ from urllib.parse import quote
35
+ auth = ""
36
+ if user or pw:
37
+ auth = f"{quote(user, safe='')}:{quote(pw, safe='')}@"
38
+ return f"http://{auth}{host}:{port}"
39
+
40
+
41
+ def parse_ngrok_tunnels(json_text: str) -> str | None:
42
+ """Return 'host:port' from ngrok's /api/tunnels JSON (first tcp tunnel)."""
43
+ try:
44
+ data = json.loads(json_text)
45
+ except json.JSONDecodeError:
46
+ return None
47
+ for t in data.get("tunnels", []):
48
+ pub = t.get("public_url", "")
49
+ if pub.startswith("tcp://"):
50
+ return pub[len("tcp://"):]
51
+ return None
52
+
53
+
54
+ def parse_bore_endpoint(text: str) -> str | None:
55
+ """Return 'bore.pub:PORT' parsed from bore's log output."""
56
+ m = re.search(r"(bore\.pub:\d+)", text)
57
+ if m:
58
+ return m.group(1)
59
+ m = re.search(r"listening at\s+([\w.-]+:\d+)", text)
60
+ return m.group(1) if m else None
61
+
62
+
63
+ def hf_candidate_tokens() -> list[str]:
64
+ """HF tokens to try: cached token, then any huggingface.co git-credential."""
65
+ out, seen = [], set()
66
+ env = dict(os.environ)
67
+ env.pop("HF_TOKEN", None)
68
+ env.pop("HUGGING_FACE_HUB_TOKEN", None)
69
+ try:
70
+ from huggingface_hub import get_token
71
+ # get_token reads HF_TOKEN env first; temporarily clear it for the cached one
72
+ saved = os.environ.pop("HF_TOKEN", None)
73
+ try:
74
+ t = get_token()
75
+ finally:
76
+ if saved is not None:
77
+ os.environ["HF_TOKEN"] = saved
78
+ if t and t not in seen:
79
+ seen.add(t); out.append(t)
80
+ except Exception:
81
+ pass
82
+ cred = os.path.join(os.path.expanduser("~"), ".git-credentials")
83
+ if os.path.exists(cred):
84
+ with open(cred, encoding="utf-8", errors="ignore") as fh:
85
+ for line in fh:
86
+ m = re.search(r"https://[^:]*:([^@]+)@huggingface\.co", line)
87
+ if m and m.group(1) not in seen:
88
+ seen.add(m.group(1)); out.append(m.group(1))
89
+ return out
90
+
91
+
92
+ def set_space_secret(repo: str, key: str, value: str, token: str | None = None) -> str:
93
+ """Set a Space secret; return the authenticated username. Raises on failure."""
94
+ from huggingface_hub import HfApi
95
+ tokens = [token] if token else hf_candidate_tokens()
96
+ last = None
97
+ for tok in tokens:
98
+ try:
99
+ api = HfApi(token=tok)
100
+ who = api.whoami()["name"]
101
+ api.add_space_secret(repo_id=repo, key=key, value=value)
102
+ return who
103
+ except Exception as exc:
104
+ last = exc
105
+ raise RuntimeError(f"Could not set secret: {last}")
106
+
107
+
108
+ def test_through_proxy(proxy_url: str) -> dict:
109
+ """Route a couple of requests through proxy_url; report egress IP + YouTube reach."""
110
+ import requests
111
+ proxies = {"http": proxy_url, "https": proxy_url}
112
+ out = {}
113
+ try:
114
+ out["egress_ip"] = requests.get("https://api.ipify.org", proxies=proxies,
115
+ timeout=25).text.strip()
116
+ except Exception as exc:
117
+ out["egress_ip"] = f"FAILED: {type(exc).__name__}: {str(exc)[:120]}"
118
+ try:
119
+ r = requests.get("https://www.youtube.com/robots.txt", proxies=proxies, timeout=25)
120
+ out["youtube"] = f"reachable (HTTP {r.status_code})"
121
+ except Exception as exc:
122
+ out["youtube"] = f"FAILED: {type(exc).__name__}: {str(exc)[:120]}"
123
+ return out
124
+
125
+
126
+ # ------------------------------------------------------------------- subprocess manager
127
+ class Proc:
128
+ """A subprocess whose stdout/stderr lines are streamed into a queue."""
129
+
130
+ def __init__(self, log_q: "queue.Queue", tag: str):
131
+ self.p = None
132
+ self.log_q = log_q
133
+ self.tag = tag
134
+
135
+ def start(self, cmd: list[str]):
136
+ self.stop()
137
+ self.log_q.put(f"[{self.tag}] $ " + " ".join(cmd))
138
+ self.p = subprocess.Popen(
139
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
140
+ text=True, bufsize=1, creationflags=_NO_WINDOW)
141
+ threading.Thread(target=self._pump, daemon=True).start()
142
+
143
+ def _pump(self):
144
+ for line in self.p.stdout:
145
+ self.log_q.put(f"[{self.tag}] " + line.rstrip())
146
+
147
+ def running(self) -> bool:
148
+ return self.p is not None and self.p.poll() is None
149
+
150
+ def stop(self):
151
+ if self.running():
152
+ try:
153
+ self.p.terminate()
154
+ except Exception:
155
+ pass
156
+ self.p = None
157
+
158
+
159
+ # ---------------------------------------------------------------------------- GUI shell
160
+ def main():
161
+ import tkinter as tk
162
+ from tkinter import ttk, messagebox
163
+
164
+ cfg = {}
165
+ if os.path.exists(CONFIG_PATH):
166
+ try:
167
+ cfg = json.load(open(CONFIG_PATH, encoding="utf-8"))
168
+ except Exception:
169
+ cfg = {}
170
+
171
+ root = tk.Tk()
172
+ root.title("TutorialMaker — Home Proxy Panel")
173
+ root.geometry("760x620")
174
+ log_q: "queue.Queue" = queue.Queue()
175
+ proxy_proc = Proc(log_q, "proxy")
176
+ tunnel_proc = Proc(log_q, "tunnel")
177
+ state = {"endpoint": None}
178
+
179
+ def v(name, default=""):
180
+ return tk.StringVar(value=str(cfg.get(name, default)))
181
+
182
+ port = v("port", "8899")
183
+ user = v("user", "tm")
184
+ pw = v("pw", secrets.token_urlsafe(10))
185
+ provider = v("provider", "ngrok")
186
+ ngrok_token = v("ngrok_token", "")
187
+ manual_ep = v("manual_ep", "")
188
+ repo = v("repo", DEFAULT_REPO)
189
+ hf_token = v("hf_token", "")
190
+ endpoint_var = tk.StringVar(value="(not running)")
191
+ yturl_var = tk.StringVar(value="")
192
+ status_var = tk.StringVar(value="idle")
193
+
194
+ def log(msg):
195
+ log_q.put(msg)
196
+
197
+ def save_cfg():
198
+ json.dump({"port": port.get(), "user": user.get(), "pw": pw.get(),
199
+ "provider": provider.get(), "ngrok_token": ngrok_token.get(),
200
+ "manual_ep": manual_ep.get(), "repo": repo.get()},
201
+ open(CONFIG_PATH, "w", encoding="utf-8"))
202
+ log(f"[cfg] saved to {CONFIG_PATH}")
203
+
204
+ def compute_yturl():
205
+ ep = state["endpoint"]
206
+ if not ep:
207
+ yturl_var.set("")
208
+ return None
209
+ host, _, p = ep.partition(":")
210
+ url = build_proxy_url(host, p, user.get(), pw.get())
211
+ yturl_var.set(url)
212
+ return url
213
+
214
+ # --- proxy ---
215
+ def start_proxy():
216
+ cmd = [sys.executable, "-m", "proxy", "--hostname", "127.0.0.1",
217
+ "--port", port.get(), "--basic-auth", f"{user.get()}:{pw.get()}"]
218
+ proxy_proc.start(cmd)
219
+ status_var.set("proxy starting…")
220
+
221
+ def stop_proxy():
222
+ proxy_proc.stop(); log("[proxy] stopped")
223
+
224
+ # --- tunnel ---
225
+ def start_tunnel():
226
+ prov = provider.get()
227
+ if prov == "manual":
228
+ ep = manual_ep.get().strip()
229
+ if not ep:
230
+ messagebox.showwarning("Manual", "Enter host:port for the manual endpoint.")
231
+ return
232
+ state["endpoint"] = ep
233
+ endpoint_var.set(ep); compute_yturl(); log(f"[tunnel] manual endpoint {ep}")
234
+ return
235
+ if prov == "ngrok":
236
+ if ngrok_token.get().strip():
237
+ subprocess.run(["ngrok", "config", "add-authtoken", ngrok_token.get().strip()],
238
+ creationflags=_NO_WINDOW)
239
+ tunnel_proc.start(["ngrok", "tcp", port.get(), "--log", "stdout"])
240
+ root.after(2500, _grab_ngrok)
241
+ elif prov == "bore":
242
+ tunnel_proc.start(["bore", "local", port.get(), "--to", "bore.pub"])
243
+ status_var.set("tunnel starting…")
244
+
245
+ def _grab_ngrok():
246
+ try:
247
+ import requests
248
+ j = requests.get("http://127.0.0.1:4040/api/tunnels", timeout=5).text
249
+ ep = parse_ngrok_tunnels(j)
250
+ if ep:
251
+ state["endpoint"] = ep; endpoint_var.set(ep); compute_yturl()
252
+ log(f"[tunnel] ngrok endpoint {ep}")
253
+ else:
254
+ log("[tunnel] ngrok: no tcp tunnel yet, retrying…"); root.after(2000, _grab_ngrok)
255
+ except Exception as exc:
256
+ log(f"[tunnel] ngrok api: {exc}"); root.after(2000, _grab_ngrok)
257
+
258
+ def stop_tunnel():
259
+ tunnel_proc.stop(); state["endpoint"] = None
260
+ endpoint_var.set("(not running)"); yturl_var.set(""); log("[tunnel] stopped")
261
+
262
+ # --- actions ---
263
+ def copy_url():
264
+ url = compute_yturl()
265
+ if url:
266
+ root.clipboard_clear(); root.clipboard_append(url); log("[yturl] copied")
267
+
268
+ def do_test():
269
+ url = compute_yturl()
270
+ if not url:
271
+ messagebox.showinfo("Test", "Start the tunnel first."); return
272
+ status_var.set("testing…")
273
+ def work():
274
+ res = test_through_proxy(url)
275
+ log(f"[test] egress IP: {res['egress_ip']}")
276
+ log(f"[test] youtube: {res['youtube']}")
277
+ status_var.set("test done")
278
+ threading.Thread(target=work, daemon=True).start()
279
+
280
+ def do_set_secret():
281
+ url = compute_yturl()
282
+ if not url:
283
+ messagebox.showinfo("Secret", "Start the tunnel first."); return
284
+ status_var.set("setting YT_PROXY…")
285
+ def work():
286
+ try:
287
+ who = set_space_secret(repo.get().strip(), "YT_PROXY", url,
288
+ hf_token.get().strip() or None)
289
+ log(f"[hf] YT_PROXY set on {repo.get()} (as {who})")
290
+ status_var.set("YT_PROXY set ✓")
291
+ except Exception as exc:
292
+ log(f"[hf] FAILED: {exc}"); status_var.set("secret failed")
293
+ threading.Thread(target=work, daemon=True).start()
294
+
295
+ # --- layout ---
296
+ pad = {"padx": 6, "pady": 3}
297
+ frm = ttk.Frame(root); frm.pack(fill="x", **pad)
298
+ r = 0
299
+ def row(label, var, show=None, width=42):
300
+ nonlocal r
301
+ ttk.Label(frm, text=label).grid(row=r, column=0, sticky="w", **pad)
302
+ e = ttk.Entry(frm, textvariable=var, width=width, show=show)
303
+ e.grid(row=r, column=1, columnspan=3, sticky="w", **pad); r += 1
304
+ return e
305
+
306
+ row("Proxy port", port, width=10)
307
+ row("Proxy user", user, width=18)
308
+ row("Proxy password", pw, width=24)
309
+ ttk.Label(frm, text="Tunnel provider").grid(row=r, column=0, sticky="w", **pad)
310
+ ttk.Combobox(frm, textvariable=provider, values=["ngrok", "bore", "manual"],
311
+ width=10, state="readonly").grid(row=r, column=1, sticky="w", **pad); r += 1
312
+ row("ngrok authtoken (ngrok only)", ngrok_token, show="*")
313
+ row("Manual endpoint host:port (manual only)", manual_ep, width=28)
314
+ row("HF Space repo", repo, width=34)
315
+ row("HF token (blank = auto-detect)", hf_token, show="*")
316
+
317
+ btns = ttk.Frame(root); btns.pack(fill="x", **pad)
318
+ for txt, fn in [("Start proxy", start_proxy), ("Stop proxy", stop_proxy),
319
+ ("Start tunnel", start_tunnel), ("Stop tunnel", stop_tunnel)]:
320
+ ttk.Button(btns, text=txt, command=fn).pack(side="left", **pad)
321
+
322
+ btns2 = ttk.Frame(root); btns2.pack(fill="x", **pad)
323
+ for txt, fn in [("Test proxy", do_test), ("Set YT_PROXY on Space", do_set_secret),
324
+ ("Copy YT_PROXY URL", copy_url), ("Save config", save_cfg),
325
+ ("New password", lambda: pw.set(secrets.token_urlsafe(10)))]:
326
+ ttk.Button(btns2, text=txt, command=fn).pack(side="left", **pad)
327
+
328
+ info = ttk.Frame(root); info.pack(fill="x", **pad)
329
+ ttk.Label(info, text="Public endpoint:").grid(row=0, column=0, sticky="w", **pad)
330
+ ttk.Label(info, textvariable=endpoint_var, foreground="#0a7").grid(row=0, column=1, sticky="w", **pad)
331
+ ttk.Label(info, text="YT_PROXY URL:").grid(row=1, column=0, sticky="w", **pad)
332
+ ttk.Entry(info, textvariable=yturl_var, width=70, state="readonly").grid(row=1, column=1, sticky="w", **pad)
333
+ ttk.Label(info, text="Status:").grid(row=2, column=0, sticky="w", **pad)
334
+ ttk.Label(info, textvariable=status_var, foreground="#06c").grid(row=2, column=1, sticky="w", **pad)
335
+
336
+ txt = tk.Text(root, height=16, wrap="word", bg="#111", fg="#ddd")
337
+ txt.pack(fill="both", expand=True, **pad)
338
+
339
+ def poll():
340
+ try:
341
+ while True:
342
+ line = log_q.get_nowait()
343
+ txt.insert("end", line + "\n"); txt.see("end")
344
+ except queue.Empty:
345
+ pass
346
+ # refresh running status
347
+ s = []
348
+ s.append("proxy:on" if proxy_proc.running() else "proxy:off")
349
+ s.append("tunnel:on" if tunnel_proc.running() else "tunnel:off")
350
+ if status_var.get() in ("idle", "proxy starting…", "tunnel starting…"):
351
+ status_var.set(" ".join(s))
352
+ root.after(400, poll)
353
+
354
+ def on_close():
355
+ proxy_proc.stop(); tunnel_proc.stop(); root.destroy()
356
+
357
+ root.protocol("WM_DELETE_WINDOW", on_close)
358
+ log("Ready. 1) Start proxy 2) Start tunnel 3) Test proxy 4) Set YT_PROXY on Space.")
359
+ log("Tip: 'Test proxy' egress IP should be your HOME IP, and YouTube should be reachable.")
360
+ poll()
361
+ root.mainloop()
362
+
363
+
364
+ if __name__ == "__main__":
365
+ main()
tools/requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ proxy.py>=2.4
2
+ huggingface_hub>=0.27
3
+ requests>=2.31