vivekchakraverty Claude Opus 4.8 commited on
Commit
12e4183
·
1 Parent(s): 21ba408

Per-user residential proxy: proxy URL field + downloadable Home Proxy Panel

Browse files

- app.py: new per-request 'Your proxy URL' field (overrides YT_PROXY secret) threaded
through run_pipeline/check_access; intro links the Home Proxy Panel GitHub repo. Merged
onto the current media-proxy/probe-battery app.
- pipeline/transcribe.py: redact proxy credentials from error hints.
- tools/home_proxy_panel.py: end-user flow (Copy my Proxy URL), bore auto-download,
frozen (--run-proxy) PyInstaller support.
- tools/home_proxy_panel.spec + build_panel.md: standalone executable build.
- README + tools/README: how to compile + use the residential proxy with this Space
(repo: github.com/vivekchakraverty/tutorialmaker-home-proxy-panel).

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

README.md CHANGED
@@ -52,11 +52,32 @@ single best YouTube video on that topic. **No video download** — acquisition i
52
  ## Notes on YouTube access
53
 
54
  The transcript and the stream-URL resolution hit YouTube directly. From a datacenter IP
55
- (like a Space) these can occasionally be **blocked**:
 
56
 
57
- - If transcript/stream resolution fails, set the optional **`YT_PROXY`** secret (a
58
- residential proxy) and/or **`YT_COOKIES`** (Netscape cookies.txt contents, raw or
59
- base64) used only as a thin fallback.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  - If the stream URL can't be resolved, the Space still produces a **text-only** tutorial.
61
 
62
  ## Local run
 
52
  ## Notes on YouTube access
53
 
54
  The transcript and the stream-URL resolution hit YouTube directly. From a datacenter IP
55
+ (like a Space) these are usually **blocked**. The fix is a **residential proxy** — and each
56
+ user can bring their own.
57
 
58
+ ### Recommended: run your own residential proxy (per user)
59
+
60
+ Paste your own proxy URL into the **Your proxy URL** field so YouTube requests for *your*
61
+ generation exit from *your* home IP. Get a proxy with the **Home Proxy Panel**:
62
+
63
+ **GitHub:** <https://github.com/vivekchakraverty/tutorialmaker-home-proxy-panel>
64
+ (also in [`tools/`](tools/) here).
65
+
66
+ 1. **Download** the prebuilt `HomeProxyPanel` app for your OS from the repo's Releases (no
67
+ Python needed), **or** run from source: `pip install -r tools/requirements.txt` then
68
+ `python tools/home_proxy_panel.py`.
69
+ 2. **Compile it yourself** (optional): `pip install pyinstaller` then
70
+ `pyinstaller tools/home_proxy_panel.spec` → `dist/HomeProxyPanel`. PyInstaller doesn't
71
+ cross-compile, so build on each OS (or use the repo's GitHub Actions matrix). Full details
72
+ in [`tools/build_panel.md`](tools/build_panel.md).
73
+ 3. **Use it:** Start proxy → Start tunnel (bore auto-downloads) → Test proxy →
74
+ **📋 Copy my Proxy URL** → paste into **Your proxy URL** above → **Generate**. Keep the
75
+ panel running during generation.
76
+
77
+ ### Fallbacks (owner)
78
+
79
+ - Set the optional **`YT_PROXY`** secret (a shared residential proxy) and/or **`YT_COOKIES`**
80
+ (Netscape cookies.txt contents, raw or base64). The per-user field above overrides `YT_PROXY`.
81
  - If the stream URL can't be resolved, the Space still produces a **text-only** tutorial.
82
 
83
  ## Local run
app.py CHANGED
@@ -104,8 +104,11 @@ def _cookiefile(workdir: str) -> str | None:
104
  return path
105
 
106
 
107
- def _resolve_proxy() -> str | None:
108
- proxy = os.environ.get("YT_PROXY", "").strip()
 
 
 
109
  return proxy or None
110
 
111
 
@@ -122,14 +125,15 @@ def _resolve_api_key(ui_key: str | None) -> str | None:
122
  return key or None
123
 
124
 
125
- def check_access():
126
  """Health check: which secrets are set, the egress IP, and YouTube reachability."""
127
  import requests
128
 
129
- proxy = _resolve_proxy()
130
  proxies = {"http": proxy, "https": proxy} if proxy else None
 
131
  lines = [
132
- f"- **Proxy** (`YT_PROXY`): {'set ✅' if proxy else 'not set ⚪'}",
133
  f"- **Media proxy** (`YT_MEDIA_PROXY`, screenshots): "
134
  f"{'set ✅' if _resolve_media_proxy() else 'not set ⚪ (falls back to YT_PROXY)'}",
135
  f"- **Cookies** (`YT_COOKIES`): {'set ✅' if os.environ.get('YT_COOKIES') else 'not set ⚪'}",
@@ -218,7 +222,7 @@ def _collect_keywords(primary_kw, secondary_kw) -> dict:
218
  return {"primary": primary, "secondary": secondary}
219
 
220
 
221
- def run_pipeline(topic, hf_token, yt_api_key, llm_model, vlm_model,
222
  w_llm, w_whisper, lead, max_shots, primary_kw, secondary_kw,
223
  content_brief="", progress=gr.Progress()):
224
  """Generator yielding (status_md, ranking_df, transcript, docx_file)."""
@@ -239,9 +243,11 @@ def run_pipeline(topic, hf_token, yt_api_key, llm_model, vlm_model,
239
  try:
240
  api_key = _resolve_api_key(yt_api_key)
241
  cookiefile = _cookiefile(workdir)
242
- proxy = _resolve_proxy()
243
  if proxy:
244
- log.append("🔐 `YT_PROXY` is set transcript + stream requests route through it.")
 
 
245
 
246
  # 1. Search ------------------------------------------------------------------
247
  progress(0.03, desc="Searching")
@@ -333,7 +339,12 @@ def build_ui():
333
  "Enter a topic, your **Hugging Face token** (LLM + vision model, billed to you) "
334
  "and a **YouTube Data API key** (for comments). The Space picks the best video by "
335
  "comment sentiment, pulls its transcript, writes an AEO-friendly tutorial, grabs "
336
- "real screenshots at the right moments, and builds a **.docx**."
 
 
 
 
 
337
  )
338
  with gr.Row():
339
  with gr.Column(scale=2):
@@ -342,6 +353,13 @@ def build_ui():
342
  placeholder="hf_… (Inference Providers permission)")
343
  yt_api_key = gr.Textbox(label="YouTube Data API key", type="password",
344
  placeholder="for comments (or set the YOUTUBE_API_KEY secret)")
 
 
 
 
 
 
 
345
  with gr.Column(scale=1):
346
  llm_model = gr.Dropdown(LLM_CHOICES, value=LLM_CHOICES[0],
347
  label="Tutorial LLM", allow_custom_value=True)
@@ -393,10 +411,10 @@ def build_ui():
393
  transcript_box = gr.Textbox(label="Transcript preview", lines=10, max_lines=20)
394
  docx_file = gr.File(label="Download tutorial (.docx)")
395
 
396
- health_btn.click(check_access, outputs=health_md)
397
  run_btn.click(
398
  run_pipeline,
399
- inputs=[topic, hf_token, yt_api_key, llm_model, vlm_model,
400
  w_llm, w_whisper, lead, max_shots, primary_kw, secondary_kw,
401
  content_brief],
402
  outputs=[status_md, ranking_df, transcript_box, docx_file],
 
104
  return path
105
 
106
 
107
+ def _resolve_proxy(ui_proxy: str | None = None) -> str | None:
108
+ """A per-user proxy pasted in the UI wins over the shared YT_PROXY secret, so each
109
+ user can route through their own residential proxy (see tools/home_proxy_panel.py).
110
+ It also backs screenshot downloads (capture_shots falls back to it)."""
111
+ proxy = (ui_proxy or "").strip() or os.environ.get("YT_PROXY", "").strip()
112
  return proxy or None
113
 
114
 
 
125
  return key or None
126
 
127
 
128
+ def check_access(yt_proxy=None):
129
  """Health check: which secrets are set, the egress IP, and YouTube reachability."""
130
  import requests
131
 
132
+ proxy = _resolve_proxy(yt_proxy)
133
  proxies = {"http": proxy, "https": proxy} if proxy else None
134
+ src = "your proxy field" if (yt_proxy or "").strip() else "YT_PROXY secret"
135
  lines = [
136
+ f"- **Proxy**: {('set ✅ (' + src + ')') if proxy else 'not set ⚪'}",
137
  f"- **Media proxy** (`YT_MEDIA_PROXY`, screenshots): "
138
  f"{'set ✅' if _resolve_media_proxy() else 'not set ⚪ (falls back to YT_PROXY)'}",
139
  f"- **Cookies** (`YT_COOKIES`): {'set ✅' if os.environ.get('YT_COOKIES') else 'not set ⚪'}",
 
222
  return {"primary": primary, "secondary": secondary}
223
 
224
 
225
+ def run_pipeline(topic, hf_token, yt_api_key, yt_proxy, llm_model, vlm_model,
226
  w_llm, w_whisper, lead, max_shots, primary_kw, secondary_kw,
227
  content_brief="", progress=gr.Progress()):
228
  """Generator yielding (status_md, ranking_df, transcript, docx_file)."""
 
243
  try:
244
  api_key = _resolve_api_key(yt_api_key)
245
  cookiefile = _cookiefile(workdir)
246
+ proxy = _resolve_proxy(yt_proxy)
247
  if proxy:
248
+ # Never log the proxy URL itself it carries user:pass credentials.
249
+ src = "your proxy" if (yt_proxy or "").strip() else "the `YT_PROXY` secret"
250
+ log.append(f"🔐 Routing transcript + screenshot requests through {src}.")
251
 
252
  # 1. Search ------------------------------------------------------------------
253
  progress(0.03, desc="Searching")
 
339
  "Enter a topic, your **Hugging Face token** (LLM + vision model, billed to you) "
340
  "and a **YouTube Data API key** (for comments). The Space picks the best video by "
341
  "comment sentiment, pulls its transcript, writes an AEO-friendly tutorial, grabs "
342
+ "real screenshots at the right moments, and builds a **.docx**.\n\n"
343
+ "> 🏠 **YouTube blocks this Space's datacenter IP.** Run your own residential proxy "
344
+ "with the **Home Proxy Panel** "
345
+ "([download / source](https://github.com/vivekchakraverty/tutorialmaker-home-proxy-panel)) "
346
+ "and paste its URL into **Your proxy URL** below — transcript + screenshots then route "
347
+ "through your home IP. Each user brings their own proxy."
348
  )
349
  with gr.Row():
350
  with gr.Column(scale=2):
 
353
  placeholder="hf_… (Inference Providers permission)")
354
  yt_api_key = gr.Textbox(label="YouTube Data API key", type="password",
355
  placeholder="for comments (or set the YOUTUBE_API_KEY secret)")
356
+ yt_proxy = gr.Textbox(
357
+ label="Your proxy URL (recommended)", type="password",
358
+ placeholder="http://user:pass@bore.pub:12345 — from the Home Proxy Panel",
359
+ info="Run the Home Proxy Panel (github.com/vivekchakraverty/"
360
+ "tutorialmaker-home-proxy-panel) on your own machine and paste its "
361
+ "proxy URL here so YouTube requests exit from your residential IP. "
362
+ "Leave blank to use the Space's shared proxy, if configured.")
363
  with gr.Column(scale=1):
364
  llm_model = gr.Dropdown(LLM_CHOICES, value=LLM_CHOICES[0],
365
  label="Tutorial LLM", allow_custom_value=True)
 
411
  transcript_box = gr.Textbox(label="Transcript preview", lines=10, max_lines=20)
412
  docx_file = gr.File(label="Download tutorial (.docx)")
413
 
414
+ health_btn.click(check_access, inputs=[yt_proxy], outputs=health_md)
415
  run_btn.click(
416
  run_pipeline,
417
+ inputs=[topic, hf_token, yt_api_key, yt_proxy, llm_model, vlm_model,
418
  w_llm, w_whisper, lead, max_shots, primary_kw, secondary_kw,
419
  content_brief],
420
  outputs=[status_md, ranking_df, transcript_box, docx_file],
pipeline/transcribe.py CHANGED
@@ -8,8 +8,17 @@ timestamps, which we normalize into ``{start, end, text}`` segments.
8
  """
9
  from __future__ import annotations
10
 
 
11
  import time
12
 
 
 
 
 
 
 
 
 
13
 
14
  class TranscriptError(RuntimeError):
15
  """Raised when no usable transcript can be fetched (disabled, blocked, none)."""
@@ -94,7 +103,7 @@ def transcript_text(segs: list[dict]) -> str:
94
 
95
  def _hint(exc: Exception) -> str:
96
  name = type(exc).__name__
97
- msg = str(exc)
98
  low = (name + " " + msg).lower()
99
  # Check specific transcript states first (note: "transcripts" contains "ip").
100
  if "disabled" in low or "transcriptsdisabled" in low:
 
8
  """
9
  from __future__ import annotations
10
 
11
+ import re
12
  import time
13
 
14
+ # Strip credentials from any proxy URL that a library exception might echo, so a pasted
15
+ # http://user:pass@host proxy never leaks into the UI/logs.
16
+ _CRED_RE = re.compile(r"(https?://)[^/@\s]+@")
17
+
18
+
19
+ def _redact(text: str) -> str:
20
+ return _CRED_RE.sub(r"\1", str(text))
21
+
22
 
23
  class TranscriptError(RuntimeError):
24
  """Raised when no usable transcript can be fetched (disabled, blocked, none)."""
 
103
 
104
  def _hint(exc: Exception) -> str:
105
  name = type(exc).__name__
106
+ msg = _redact(str(exc))
107
  low = (name + " " + msg).lower()
108
  # Check specific transcript states first (note: "transcripts" contains "ip").
109
  if "disabled" in low or "transcriptsdisabled" in low:
tools/README.md CHANGED
@@ -1,10 +1,23 @@
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
@@ -27,12 +40,16 @@ pip install -r tools/requirements.txt
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
  ```
@@ -43,19 +60,21 @@ python tools/home_proxy_panel.py
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
 
 
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**. You then paste the
6
+ resulting **proxy URL into the Space's *Your proxy URL* field** the Space's YouTube
7
+ requests for *your* generation exit from *your* IP and stop getting blocked.
8
+
9
+ **Per-user by design:** every user runs their own panel and uses their own proxy URL for
10
+ their own generation, so there's no single shared residential proxy to run or bottleneck.
11
+ Combined with the per-user Hugging Face token the Space already asks for, each user is fully
12
+ self-served (their own compute + their own IP).
13
+
14
+ > **Just want to run it?** Grab the prebuilt **HomeProxyPanel** app for your OS (no Python
15
+ > needed) — see [build_panel.md](build_panel.md) for how the executables are produced. Or
16
+ > run the script directly per the steps below.
17
+
18
+ > **Owner note:** the **Set on Space (owner)** button writes the global `YT_PROXY` secret and
19
+ > only works if you have HF write access to the Space; end users don't need it — they use the
20
+ > *Your proxy URL* field instead.
21
 
22
  > **Why a TCP tunnel (not Cloudflare/Tailscale):** a forward proxy speaks HTTP `CONNECT`,
23
  > which only survives a transparent **TCP** tunnel. HTTP *reverse* tunnels (Cloudflare
 
40
  3. Copy your authtoken from the ngrok dashboard → you'll paste it into the panel.
41
 
42
  **Option B — bore (no account, less reliable):**
43
+ 1. Nothing to install — the panel **auto-downloads** the right `bore` binary for your OS on
44
+ first use (into the folder next to the app). It uses the shared public `bore.pub` server.
45
+ 2. (Optional) To pre-place it, download `bore` from
46
+ <https://github.com/ekzhang/bore/releases> and put it on your PATH or next to the app.
47
 
48
  ## 3. Launch the panel
49
 
50
+ Prebuilt app (no Python): double-click **HomeProxyPanel** (see [build_panel.md](build_panel.md)).
51
+
52
+ Or from source:
53
  ```bash
54
  python tools/home_proxy_panel.py
55
  ```
 
60
  the password — it protects your public proxy from being used by strangers.
61
  2. Pick the **Tunnel provider** (`ngrok`, `bore`, or `manual`). For ngrok, paste your
62
  **authtoken**.
63
+ 3. Click **Start proxy** → it runs the forward proxy on `127.0.0.1:<port>`.
64
  4. Click **Start tunnel** → the **Public endpoint** field fills in
65
+ (`x.tcp.ngrok.io:NNNN` or `bore.pub:NNNN`) and **Your proxy URL** is computed.
66
  5. Click **Test proxy** → in the log, the **egress IP should be your home IP** and YouTube
67
  should be **reachable**. If so, the proxy works.
68
+ 6. Click **📋 Copy my Proxy URL**, then paste it into the TutorialMaker Space's **Your proxy
69
+ URL** field and click **Generate**. Keep the panel running while you generate.
70
+ - *(Space owner only)* **Set on Space (owner)** instead writes the global `YT_PROXY`
71
+ secret for everyone — end users don't use this.
72
 
73
  ## 5. Verify on the Space
74
 
75
+ Paste your proxy URL into **Your proxy URL** and run a topic. **Check YouTube access** should
76
+ show your home IP as the egress, and the **transcript** and **screenshot** steps should
77
+ succeed instead of failing with an SSL/TLS block.
78
 
79
  ## Keep it running
80
 
tools/build_panel.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Building the Home Proxy Panel as a standalone app
2
+
3
+ This produces a **double-click executable** of `home_proxy_panel.py` so end users can run
4
+ their own residential proxy **without installing Python**. Each user runs it, clicks
5
+ **Copy my Proxy URL**, and pastes that URL into the TutorialMaker Space's *Your proxy URL*
6
+ field — their generation then routes YouTube requests through their own home IP.
7
+
8
+ ## ⚠️ One binary per OS — PyInstaller does not cross-compile
9
+
10
+ You must build **on each target OS** (Windows `.exe` on Windows, macOS binary on macOS,
11
+ Linux ELF on Linux). Options:
12
+ - Build on the three machines you have, or
13
+ - Use a GitHub Actions matrix (`windows-latest`, `macos-latest`, `ubuntu-latest`) and
14
+ attach the artifacts to a Release. A ready starter workflow is in the section below.
15
+
16
+ ## Build steps (run on the target OS)
17
+
18
+ ```bash
19
+ pip install pyinstaller proxy.py huggingface_hub requests
20
+ # from the TutorialMaker repo root:
21
+ pyinstaller tools/home_proxy_panel.spec
22
+ ```
23
+
24
+ Output: `dist/HomeProxyPanel` (`.exe` on Windows). Ship that single file.
25
+
26
+ Notes:
27
+ - `proxy.py` (the forward proxy) is bundled. The app runs it by re-invoking itself with a
28
+ hidden `--run-proxy` flag, so it works even though `sys.executable` is the packaged app.
29
+ - `bore` is **not** bundled by default — the app auto-downloads the correct release binary
30
+ for the host on first use (into the folder next to the executable). To bundle it instead
31
+ (offline installs), drop `bore`/`bore.exe` next to `home_proxy_panel.spec` before building
32
+ — the spec picks it up automatically.
33
+ - `console=False` makes it a windowed GUI app. Set `console=True` in the spec temporarily if
34
+ you need to see tracebacks while debugging a build.
35
+
36
+ ## macOS gotchas
37
+ - Unsigned apps are quarantined: users run `xattr -dr com.apple.quarantine HomeProxyPanel`
38
+ or right-click → Open the first time. For public distribution, codesign + notarize.
39
+
40
+ ## GitHub Actions matrix (optional starter)
41
+
42
+ ```yaml
43
+ name: build-proxy-panel
44
+ on: { workflow_dispatch: {} }
45
+ jobs:
46
+ build:
47
+ strategy:
48
+ matrix:
49
+ os: [windows-latest, macos-latest, ubuntu-latest]
50
+ runs-on: ${{ matrix.os }}
51
+ steps:
52
+ - uses: actions/checkout@v4
53
+ - uses: actions/setup-python@v5
54
+ with: { python-version: "3.12" }
55
+ - run: pip install pyinstaller proxy.py huggingface_hub requests
56
+ - run: pyinstaller tools/home_proxy_panel.spec
57
+ - uses: actions/upload-artifact@v4
58
+ with:
59
+ name: HomeProxyPanel-${{ matrix.os }}
60
+ path: dist/*
61
+ ```
62
+
63
+ ## Verify a built binary
64
+ On a machine **without** Python:
65
+ 1. Launch `HomeProxyPanel`.
66
+ 2. **Start proxy** → **Start tunnel** (bore auto-downloads on first use) → **Test proxy**:
67
+ the log's egress IP should be your home IP and YouTube should be reachable.
68
+ 3. **Copy my Proxy URL**, paste it into the Space's *Your proxy URL* field, and generate.
tools/home_proxy_panel.py CHANGED
@@ -16,17 +16,100 @@ 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:
@@ -213,8 +296,14 @@ def main():
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
 
@@ -239,7 +328,19 @@ def main():
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():
@@ -320,15 +421,16 @@ def main():
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)
@@ -355,11 +457,21 @@ def main():
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()
 
 
 
 
 
 
 
 
 
16
 
17
  import json
18
  import os
19
+ import platform
20
  import queue
21
  import re
22
  import secrets
23
+ import shutil
24
  import subprocess
25
  import sys
26
+ import tarfile
27
  import threading
28
+ import zipfile
29
 
30
  CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".tutorialmaker_proxy.json")
31
  DEFAULT_REPO = "vivekchakraverty/TutorialMaker"
32
  _NO_WINDOW = 0x08000000 if os.name == "nt" else 0
33
 
34
+ # Where to keep a downloaded bore binary. When frozen by PyInstaller sys.frozen is set and
35
+ # the binary lives next to the executable; otherwise next to this script.
36
+ APP_DIR = os.path.dirname(sys.executable if getattr(sys, "frozen", False)
37
+ else os.path.abspath(__file__))
38
+
39
+
40
+ def _bore_target_triple() -> tuple[str, str, str] | None:
41
+ """(rust-triple, archive-ext, binary-name) for the current OS/arch, or None."""
42
+ sysname = platform.system()
43
+ mach = platform.machine().lower()
44
+ is_arm = mach in ("arm64", "aarch64")
45
+ if sysname == "Windows":
46
+ return ("x86_64-pc-windows-msvc", ".zip", "bore.exe")
47
+ if sysname == "Darwin":
48
+ triple = "aarch64-apple-darwin" if is_arm else "x86_64-apple-darwin"
49
+ return (triple, ".tar.gz", "bore")
50
+ if sysname == "Linux":
51
+ triple = "aarch64-unknown-linux-musl" if is_arm else "x86_64-unknown-linux-musl"
52
+ return (triple, ".tar.gz", "bore")
53
+ return None
54
+
55
+
56
+ def ensure_bore(log=lambda m: None) -> str | None:
57
+ """Return a runnable bore path: PATH copy, a previously downloaded one, or a freshly
58
+ downloaded release binary for this OS/arch. Returns None if it can't be obtained."""
59
+ found = shutil.which("bore")
60
+ if found:
61
+ return found
62
+ tri = _bore_target_triple()
63
+ if not tri:
64
+ log("[bore] no prebuilt binary for this platform — install bore manually.")
65
+ return None
66
+ triple, ext, binname = tri
67
+ local = os.path.join(APP_DIR, binname)
68
+ if os.path.exists(local):
69
+ return local
70
+
71
+ import urllib.request
72
+ api = "https://api.github.com/repos/ekzhang/bore/releases/latest"
73
+ try:
74
+ log("[bore] not found — fetching latest release info…")
75
+ req = urllib.request.Request(api, headers={"User-Agent": "tutorialmaker-panel"})
76
+ with urllib.request.urlopen(req, timeout=30) as r:
77
+ rel = json.load(r)
78
+ asset = next((a for a in rel.get("assets", [])
79
+ if triple in a["name"] and a["name"].endswith(ext)), None)
80
+ if not asset:
81
+ log(f"[bore] no asset matching {triple}{ext} in the latest release.")
82
+ return None
83
+ url = asset["browser_download_url"]
84
+ archive = os.path.join(APP_DIR, asset["name"])
85
+ log(f"[bore] downloading {asset['name']}…")
86
+ dl = urllib.request.Request(url, headers={"User-Agent": "tutorialmaker-panel"})
87
+ with urllib.request.urlopen(dl, timeout=120) as resp, open(archive, "wb") as fh:
88
+ shutil.copyfileobj(resp, fh)
89
+ if ext == ".zip":
90
+ with zipfile.ZipFile(archive) as z:
91
+ z.extract(binname, APP_DIR)
92
+ else:
93
+ with tarfile.open(archive) as t:
94
+ member = next((m for m in t.getmembers()
95
+ if os.path.basename(m.name) == binname), None)
96
+ if member is None:
97
+ log("[bore] archive did not contain the bore binary.")
98
+ return None
99
+ member.name = binname
100
+ t.extract(member, APP_DIR)
101
+ try:
102
+ os.remove(archive)
103
+ except OSError:
104
+ pass
105
+ if os.name != "nt":
106
+ os.chmod(local, 0o755)
107
+ log(f"[bore] ready at {local}")
108
+ return local
109
+ except Exception as exc:
110
+ log(f"[bore] download failed: {type(exc).__name__}: {str(exc)[:160]}")
111
+ return None
112
+
113
 
114
  # ----------------------------------------------------------------- pure helpers (tested)
115
  def build_proxy_url(host: str, port, user: str, pw: str) -> str:
 
296
 
297
  # --- proxy ---
298
  def start_proxy():
299
+ auth = f"{user.get()}:{pw.get()}"
300
+ pargs = ["--hostname", "127.0.0.1", "--port", port.get(), "--basic-auth", auth]
301
+ if getattr(sys, "frozen", False):
302
+ # In a PyInstaller build sys.executable is THIS app, not python, so `-m proxy`
303
+ # won't work — re-invoke ourselves with the --run-proxy sentinel (see __main__).
304
+ cmd = [sys.executable, "--run-proxy", *pargs]
305
+ else:
306
+ cmd = [sys.executable, "-m", "proxy", *pargs]
307
  proxy_proc.start(cmd)
308
  status_var.set("proxy starting…")
309
 
 
328
  tunnel_proc.start(["ngrok", "tcp", port.get(), "--log", "stdout"])
329
  root.after(2500, _grab_ngrok)
330
  elif prov == "bore":
331
+ status_var.set("preparing bore")
332
+ def work():
333
+ bore = ensure_bore(log)
334
+ if not bore:
335
+ log("[tunnel] bore unavailable — download bore.exe from "
336
+ "https://github.com/ekzhang/bore/releases or pick ngrok.")
337
+ status_var.set("bore unavailable")
338
+ return
339
+ root.after(0, lambda: tunnel_proc.start(
340
+ [bore, "local", port.get(), "--to", "bore.pub"]))
341
+ status_var.set("tunnel starting…")
342
+ threading.Thread(target=work, daemon=True).start()
343
+ return
344
  status_var.set("tunnel starting…")
345
 
346
  def _grab_ngrok():
 
421
  ttk.Button(btns, text=txt, command=fn).pack(side="left", **pad)
422
 
423
  btns2 = ttk.Frame(root); btns2.pack(fill="x", **pad)
424
+ for txt, fn in [("📋 Copy my Proxy URL", copy_url), ("Test proxy", do_test),
425
+ ("Save config", save_cfg),
426
+ ("New password", lambda: pw.set(secrets.token_urlsafe(10))),
427
+ ("Set on Space (owner)", do_set_secret)]:
428
  ttk.Button(btns2, text=txt, command=fn).pack(side="left", **pad)
429
 
430
  info = ttk.Frame(root); info.pack(fill="x", **pad)
431
  ttk.Label(info, text="Public endpoint:").grid(row=0, column=0, sticky="w", **pad)
432
  ttk.Label(info, textvariable=endpoint_var, foreground="#0a7").grid(row=0, column=1, sticky="w", **pad)
433
+ ttk.Label(info, text="Your proxy URL:").grid(row=1, column=0, sticky="w", **pad)
434
  ttk.Entry(info, textvariable=yturl_var, width=70, state="readonly").grid(row=1, column=1, sticky="w", **pad)
435
  ttk.Label(info, text="Status:").grid(row=2, column=0, sticky="w", **pad)
436
  ttk.Label(info, textvariable=status_var, foreground="#06c").grid(row=2, column=1, sticky="w", **pad)
 
457
  proxy_proc.stop(); tunnel_proc.stop(); root.destroy()
458
 
459
  root.protocol("WM_DELETE_WINDOW", on_close)
460
+ log("Ready. 1) Start proxy 2) Start tunnel 3) Test proxy 4) Copy my Proxy URL")
461
+ log(" → paste that URL into the TutorialMaker Space's 'Your proxy URL' field, then Generate.")
462
  log("Tip: 'Test proxy' egress IP should be your HOME IP, and YouTube should be reachable.")
463
+ log("(bore auto-downloads on first use; 'Set on Space (owner)' is only for the Space owner.)")
464
  poll()
465
  root.mainloop()
466
 
467
 
468
  if __name__ == "__main__":
469
+ # Frozen (PyInstaller) re-entry: act as `python -m proxy` when asked, so the packaged
470
+ # executable can run the forward proxy as a normal killable subprocess of itself.
471
+ if "--run-proxy" in sys.argv:
472
+ _i = sys.argv.index("--run-proxy")
473
+ import proxy
474
+ sys.argv = ["proxy", *sys.argv[_i + 1:]]
475
+ proxy.main()
476
+ else:
477
+ main()
tools/home_proxy_panel.spec ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PyInstaller spec for the TutorialMaker Home Proxy Panel.
2
+ #
3
+ # Build (on the SAME OS you want to target — PyInstaller does NOT cross-compile):
4
+ # pip install pyinstaller proxy.py huggingface_hub requests
5
+ # pyinstaller tools/home_proxy_panel.spec
6
+ # Output: dist/HomeProxyPanel(.exe)
7
+ #
8
+ # The forward proxy (proxy.py) is bundled and launched by the app re-invoking itself with
9
+ # the --run-proxy sentinel (see the __main__ guard in home_proxy_panel.py). `bore` is NOT
10
+ # bundled here — the app downloads the correct release binary for the host on first use;
11
+ # to ship it inside the exe instead, drop a `bore`/`bore.exe` next to this spec and add it
12
+ # to `datas` below.
13
+ import os
14
+
15
+ block_cipher = None
16
+
17
+ # SPECPATH is injected by PyInstaller = the directory holding this spec (…/tools).
18
+ _here = SPECPATH
19
+ _script = os.path.join(_here, "home_proxy_panel.py")
20
+ _bore = "bore.exe" if os.name == "nt" else "bore"
21
+ _bore_path = os.path.join(_here, _bore)
22
+ datas = [(_bore_path, ".")] if os.path.exists(_bore_path) else []
23
+
24
+ a = Analysis(
25
+ [_script],
26
+ pathex=[_here],
27
+ binaries=[],
28
+ datas=datas,
29
+ # proxy.py loads its plugins by dotted path at runtime, so PyInstaller's static
30
+ # analysis misses them — pull the whole package in.
31
+ hiddenimports=["proxy"],
32
+ hookspath=[],
33
+ hooksconfig={},
34
+ runtime_hooks=[],
35
+ excludes=[],
36
+ cipher=block_cipher,
37
+ )
38
+ # Ensure proxy.py's data/plugin modules are collected.
39
+ try:
40
+ from PyInstaller.utils.hooks import collect_submodules, collect_data_files
41
+ a.hiddenimports += collect_submodules("proxy")
42
+ a.datas += collect_data_files("proxy")
43
+ except Exception:
44
+ pass
45
+
46
+ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
47
+
48
+ exe = EXE(
49
+ pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [],
50
+ name="HomeProxyPanel",
51
+ debug=False,
52
+ bootloader_ignore_signals=False,
53
+ strip=False,
54
+ upx=True,
55
+ runtime_tmpdir=None,
56
+ console=False, # windowed GUI app (no console)
57
+ )