Spaces:
Sleeping
Sleeping
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 +25 -4
- app.py +29 -11
- pipeline/transcribe.py +10 -1
- tools/README.md +33 -14
- tools/build_panel.md +68 -0
- tools/home_proxy_panel.py +121 -9
- tools/home_proxy_panel.spec +57 -0
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
|
|
|
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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**
|
| 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 |
-
|
|
|
|
|
|
|
| 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**
|
| 6 |
-
|
| 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.
|
| 31 |
-
|
| 32 |
-
2.
|
|
|
|
| 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
|
| 47 |
4. Click **Start tunnel** → the **Public endpoint** field fills in
|
| 48 |
-
(`x.tcp.ngrok.io:NNNN` or `bore.pub:NNNN`) and
|
| 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 **
|
| 52 |
-
|
| 53 |
-
-
|
|
|
|
| 54 |
|
| 55 |
## 5. Verify on the Space
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
|
|
|
| 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 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 [("
|
| 324 |
-
("
|
| 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="
|
| 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)
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
)
|