superbrain-server 1.0.2-beta.1 → 1.0.2-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/package.json +25 -23
- package/payload/api.py +6 -4
- package/payload/config/model_rankings.json +20 -20
- package/payload/core/database.py +2 -2
- package/payload/start.py +166 -142
package/README.md
CHANGED
|
@@ -25,6 +25,24 @@ superbrain-server
|
|
|
25
25
|
npx -y superbrain-server@beta
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
### GitHub Packages (auth + install)
|
|
29
|
+
|
|
30
|
+
Note: This is only needed for GitHub Packages. It is not required for npmjs (`superbrain-server`) installs.
|
|
31
|
+
|
|
32
|
+
Use a GitHub token with `read:packages`, then run:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm config set @sidinsearch:registry https://npm.pkg.github.com
|
|
36
|
+
npm config set //npm.pkg.github.com/:_authToken YOUR_GITHUB_TOKEN
|
|
37
|
+
npx -y @sidinsearch/superbrain-server@latest
|
|
38
|
+
# beta channel
|
|
39
|
+
npx -y @sidinsearch/superbrain-server@beta
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
GitHub Packages docs: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry
|
|
43
|
+
|
|
44
|
+
GitHub Packages is separate from the npmjs package page and may not appear in the repository Packages tab until it has been published and linked there.
|
|
45
|
+
|
|
28
46
|
## What It Does on First Run
|
|
29
47
|
|
|
30
48
|
1. Unpacks backend files into `~/.superbrain-server`
|
package/package.json
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "superbrain-server",
|
|
3
|
-
"version": "1.0.2-beta.
|
|
4
|
-
"description": "1-Line Auto-Installer and Server Execution wrapper for SuperBrain",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"superbrain-server": "bin/superbrain.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "node scripts/build.js"
|
|
11
|
-
},
|
|
12
|
-
"keywords": [
|
|
13
|
-
"superbrain",
|
|
14
|
-
"server"
|
|
15
|
-
],
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "superbrain-server",
|
|
3
|
+
"version": "1.0.2-beta.3",
|
|
4
|
+
"description": "1-Line Auto-Installer and Server Execution wrapper for SuperBrain",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"superbrain-server": "bin/superbrain.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "node scripts/build.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"superbrain",
|
|
14
|
+
"server"
|
|
15
|
+
],
|
|
16
|
+
"repository": "https://github.com/sidinsearch/superbrain.git",
|
|
17
|
+
"homepage": "https://github.com/sidinsearch/superbrain#readme",
|
|
18
|
+
"author": "sidinsearch",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {},
|
|
21
|
+
"files": [
|
|
22
|
+
"bin/",
|
|
23
|
+
"payload/"
|
|
24
|
+
]
|
|
25
|
+
}
|
package/payload/api.py
CHANGED
|
@@ -72,17 +72,19 @@ def load_or_create_api_token():
|
|
|
72
72
|
|
|
73
73
|
API_TOKEN = load_or_create_api_token()
|
|
74
74
|
|
|
75
|
-
async def verify_token(request: Request, x_api_key: str = Header(
|
|
75
|
+
async def verify_token(request: Request, x_api_key: str = Header(None, description="Access Token for authentication")):
|
|
76
76
|
"""
|
|
77
77
|
Verify authentication using Access Token.
|
|
78
|
+
Can be passed in X-API-Key header or token query parameter.
|
|
78
79
|
"""
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
actual_token = x_api_key or request.query_params.get("token")
|
|
81
|
+
if actual_token != API_TOKEN:
|
|
82
|
+
logger.warning("Invalid Access Token attempt from IP: %s", request.client.host if hasattr(request, 'client') and request.client else 'unknown')
|
|
81
83
|
raise HTTPException(
|
|
82
84
|
status_code=401,
|
|
83
85
|
detail="Invalid Access Token. Use the token from backend/token.txt."
|
|
84
86
|
)
|
|
85
|
-
return
|
|
87
|
+
return actual_token
|
|
86
88
|
|
|
87
89
|
# Initialize FastAPI app
|
|
88
90
|
app = FastAPI(
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"groq_gpt_oss_20b": {
|
|
3
3
|
"key": "groq_gpt_oss_20b",
|
|
4
|
-
"avg_response_s": 1.
|
|
5
|
-
"success_count":
|
|
4
|
+
"avg_response_s": 1.325513668372554,
|
|
5
|
+
"success_count": 50,
|
|
6
6
|
"fail_count": 1,
|
|
7
7
|
"down_until": null,
|
|
8
|
-
"last_used": "2026-
|
|
8
|
+
"last_used": "2026-04-06T17:49:12.415601",
|
|
9
9
|
"last_error": null,
|
|
10
10
|
"base_priority": 0.5
|
|
11
11
|
},
|
|
@@ -323,8 +323,8 @@
|
|
|
323
323
|
"key": "gemini_25_flash_lite_vision",
|
|
324
324
|
"avg_response_s": 6.709401964075168,
|
|
325
325
|
"success_count": 14,
|
|
326
|
-
"fail_count":
|
|
327
|
-
"down_until": "2026-04-
|
|
326
|
+
"fail_count": 10,
|
|
327
|
+
"down_until": "2026-04-06T17:54:03.900203",
|
|
328
328
|
"last_used": "2026-02-24T09:45:09.831171",
|
|
329
329
|
"last_error": "No module named 'google.generativeai'",
|
|
330
330
|
"base_priority": 1.5
|
|
@@ -333,8 +333,8 @@
|
|
|
333
333
|
"key": "gemini_25_pro_vision",
|
|
334
334
|
"avg_response_s": null,
|
|
335
335
|
"success_count": 0,
|
|
336
|
-
"fail_count":
|
|
337
|
-
"down_until": "2026-04-
|
|
336
|
+
"fail_count": 13,
|
|
337
|
+
"down_until": "2026-04-06T17:54:03.898186",
|
|
338
338
|
"last_used": null,
|
|
339
339
|
"last_error": "No module named 'google.generativeai'",
|
|
340
340
|
"base_priority": 2
|
|
@@ -353,8 +353,8 @@
|
|
|
353
353
|
"key": "gemini_3_pro_vision",
|
|
354
354
|
"avg_response_s": null,
|
|
355
355
|
"success_count": 0,
|
|
356
|
-
"fail_count":
|
|
357
|
-
"down_until": "2026-04-
|
|
356
|
+
"fail_count": 12,
|
|
357
|
+
"down_until": "2026-04-06T17:54:03.899185",
|
|
358
358
|
"last_used": null,
|
|
359
359
|
"last_error": "No module named 'google.generativeai'",
|
|
360
360
|
"base_priority": 3
|
|
@@ -363,8 +363,8 @@
|
|
|
363
363
|
"key": "gemini_31_pro_vision",
|
|
364
364
|
"avg_response_s": null,
|
|
365
365
|
"success_count": 0,
|
|
366
|
-
"fail_count":
|
|
367
|
-
"down_until": "2026-04-
|
|
366
|
+
"fail_count": 11,
|
|
367
|
+
"down_until": "2026-04-06T17:54:03.901209",
|
|
368
368
|
"last_used": null,
|
|
369
369
|
"last_error": "No module named 'google.generativeai'",
|
|
370
370
|
"base_priority": 3.5
|
|
@@ -373,8 +373,8 @@
|
|
|
373
373
|
"key": "gemini_20_flash_vision",
|
|
374
374
|
"avg_response_s": null,
|
|
375
375
|
"success_count": 0,
|
|
376
|
-
"fail_count":
|
|
377
|
-
"down_until": "2026-04-
|
|
376
|
+
"fail_count": 10,
|
|
377
|
+
"down_until": "2026-04-06T17:54:03.901209",
|
|
378
378
|
"last_used": null,
|
|
379
379
|
"last_error": "No module named 'google.generativeai'",
|
|
380
380
|
"base_priority": 4
|
|
@@ -383,8 +383,8 @@
|
|
|
383
383
|
"key": "gemini_20_flash_lite_vision",
|
|
384
384
|
"avg_response_s": null,
|
|
385
385
|
"success_count": 0,
|
|
386
|
-
"fail_count":
|
|
387
|
-
"down_until": "2026-04-
|
|
386
|
+
"fail_count": 10,
|
|
387
|
+
"down_until": "2026-04-06T17:54:03.902186",
|
|
388
388
|
"last_used": null,
|
|
389
389
|
"last_error": "No module named 'google.generativeai'",
|
|
390
390
|
"base_priority": 4.5
|
|
@@ -393,19 +393,19 @@
|
|
|
393
393
|
"key": "gemini_15_flash_vision",
|
|
394
394
|
"avg_response_s": null,
|
|
395
395
|
"success_count": 0,
|
|
396
|
-
"fail_count":
|
|
397
|
-
"down_until": "2026-04-
|
|
396
|
+
"fail_count": 10,
|
|
397
|
+
"down_until": "2026-04-06T17:54:03.903185",
|
|
398
398
|
"last_used": null,
|
|
399
399
|
"last_error": "No module named 'google.generativeai'",
|
|
400
400
|
"base_priority": 4.8
|
|
401
401
|
},
|
|
402
402
|
"groq_llama4_scout_vision": {
|
|
403
403
|
"key": "groq_llama4_scout_vision",
|
|
404
|
-
"avg_response_s":
|
|
405
|
-
"success_count":
|
|
404
|
+
"avg_response_s": 1.9110158948201241,
|
|
405
|
+
"success_count": 30,
|
|
406
406
|
"fail_count": 0,
|
|
407
407
|
"down_until": null,
|
|
408
|
-
"last_used": "2026-
|
|
408
|
+
"last_used": "2026-04-06T17:49:07.508119",
|
|
409
409
|
"last_error": null,
|
|
410
410
|
"base_priority": 5
|
|
411
411
|
},
|
package/payload/core/database.py
CHANGED
|
@@ -315,7 +315,7 @@ class Database:
|
|
|
315
315
|
try:
|
|
316
316
|
cur = self._conn.cursor()
|
|
317
317
|
|
|
318
|
-
cur.execute("SELECT COUNT(*) FROM analyses")
|
|
318
|
+
cur.execute("SELECT COUNT(*) FROM analyses WHERE (is_hidden IS NULL OR is_hidden = 0)")
|
|
319
319
|
total = cur.fetchone()[0]
|
|
320
320
|
|
|
321
321
|
cur.execute("SELECT COUNT(*) FROM collections")
|
|
@@ -323,7 +323,7 @@ class Database:
|
|
|
323
323
|
|
|
324
324
|
cur.execute(
|
|
325
325
|
"SELECT COALESCE(category,'Uncategorized') as cat, COUNT(*) as cnt "
|
|
326
|
-
"FROM analyses GROUP BY cat"
|
|
326
|
+
"FROM analyses WHERE (is_hidden IS NULL OR is_hidden = 0) GROUP BY cat"
|
|
327
327
|
)
|
|
328
328
|
category_counts = {r["cat"]: r["cnt"] for r in cur.fetchall()}
|
|
329
329
|
|
package/payload/start.py
CHANGED
|
@@ -118,17 +118,57 @@ def run_q(cmd, **kwargs):
|
|
|
118
118
|
return subprocess.run(cmd, check=True, capture_output=True, text=True, **kwargs)
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
def _load_saved_api_keys() -> dict[str, str]:
|
|
122
|
+
"""Load saved API keys and credentials from config/.api_keys."""
|
|
123
|
+
keys: dict[str, str] = {}
|
|
124
|
+
if API_KEYS.exists():
|
|
125
|
+
for line in API_KEYS.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
126
|
+
line = line.strip()
|
|
127
|
+
if "=" in line and not line.startswith("#"):
|
|
128
|
+
k, _, v = line.partition("=")
|
|
129
|
+
keys[k.strip()] = v.strip()
|
|
130
|
+
return keys
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
CORE_PACKAGES = [
|
|
134
|
+
"fastapi>=0.111.0",
|
|
135
|
+
"uvicorn[standard]>=0.29.0",
|
|
136
|
+
"pydantic>=2.0.0",
|
|
137
|
+
"python-multipart>=0.0.9",
|
|
138
|
+
"requests>=2.31.0",
|
|
139
|
+
"httpx>=0.27.0",
|
|
140
|
+
"groq>=0.9.0",
|
|
141
|
+
"google-genai>=0.8.0",
|
|
142
|
+
"beautifulsoup4>=4.12.0",
|
|
143
|
+
"trafilatura>=1.12.0",
|
|
144
|
+
"newspaper4k>=0.9.0",
|
|
145
|
+
"lxml>=5.0.0",
|
|
146
|
+
"lxml_html_clean>=0.1.0",
|
|
147
|
+
"htmldate>=1.9.0",
|
|
148
|
+
"instaloader>=4.11.0",
|
|
149
|
+
"rich>=13.0.0",
|
|
150
|
+
"segno>=1.6.0",
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
|
|
121
154
|
def ensure_runtime_dependencies():
|
|
122
155
|
"""Install must-have runtime packages if missing in the active venv."""
|
|
123
156
|
required = [
|
|
157
|
+
("fastapi", "fastapi"),
|
|
158
|
+
("uvicorn", "uvicorn"),
|
|
124
159
|
("multipart", "python-multipart"),
|
|
160
|
+
("instaloader", "instaloader"),
|
|
161
|
+
("segno", "segno"),
|
|
125
162
|
]
|
|
126
163
|
missing: list[str] = []
|
|
127
164
|
|
|
128
165
|
for module_name, package_name in required:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
# Check if the module is actually importable in the virtual environment
|
|
167
|
+
rc = subprocess.run(
|
|
168
|
+
[str(VENV_PYTHON), "-c", f"import {module_name}"],
|
|
169
|
+
capture_output=True
|
|
170
|
+
).returncode
|
|
171
|
+
if rc != 0:
|
|
132
172
|
missing.append(package_name)
|
|
133
173
|
|
|
134
174
|
if not missing:
|
|
@@ -179,79 +219,16 @@ def setup_venv():
|
|
|
179
219
|
# Step 2 — Install Dependencies
|
|
180
220
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
181
221
|
def install_deps():
|
|
182
|
-
h1("Step 2 of 7 — Installing Python Dependencies")
|
|
183
|
-
req = BASE_DIR / "requirements.txt"
|
|
184
|
-
if not req.exists():
|
|
185
|
-
err("requirements.txt not found — cannot install dependencies.")
|
|
186
|
-
sys.exit(1)
|
|
222
|
+
h1("Step 2 of 7 — Installing Core Python Dependencies")
|
|
187
223
|
|
|
188
224
|
h2("Upgrading pip …")
|
|
189
225
|
run([str(VENV_PYTHON), "-m", "pip", "install", "--quiet", "--upgrade", "pip"])
|
|
190
226
|
ok("pip up to date")
|
|
191
227
|
|
|
192
|
-
h2("Installing
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
cmd = [str(VENV_PIP), "install", "--progress-bar", "off", "-r", str(req)]
|
|
197
|
-
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
198
|
-
text=True, bufsize=1)
|
|
199
|
-
|
|
200
|
-
collecting: list[str] = []
|
|
201
|
-
n_cached = 0
|
|
202
|
-
n_download = 0
|
|
203
|
-
n_install = 0
|
|
204
|
-
current_pkg = ""
|
|
205
|
-
|
|
206
|
-
for raw in proc.stdout: # type: ignore[union-attr]
|
|
207
|
-
line = raw.rstrip()
|
|
208
|
-
if not line:
|
|
209
|
-
continue
|
|
210
|
-
|
|
211
|
-
if line.startswith("Collecting "):
|
|
212
|
-
pkg = line.split()[1]
|
|
213
|
-
current_pkg = pkg
|
|
214
|
-
collecting.append(pkg)
|
|
215
|
-
idx = len(collecting)
|
|
216
|
-
print(f" {CYAN}↓{RESET} [{idx:>3}] {BOLD}{pkg}{RESET}")
|
|
217
|
-
|
|
218
|
-
elif "Downloading" in line and ".whl" in line or ".tar.gz" in line:
|
|
219
|
-
# e.g. " Downloading fastapi-0.111.0-py3..whl (92 kB)"
|
|
220
|
-
parts = line.strip().split()
|
|
221
|
-
if len(parts) >= 2:
|
|
222
|
-
filename = parts[1]
|
|
223
|
-
size_str = " ".join(parts[2:]).strip("()")
|
|
224
|
-
print(f" {DIM}↓ {filename} {size_str}{RESET}")
|
|
225
|
-
n_download += 1
|
|
226
|
-
|
|
227
|
-
elif line.strip().startswith("Requirement already satisfied"):
|
|
228
|
-
n_cached += 1
|
|
229
|
-
|
|
230
|
-
elif line.startswith("Installing collected packages:"):
|
|
231
|
-
pkgs = line.split(":", 1)[1].strip()
|
|
232
|
-
n_install = len(pkgs.split(","))
|
|
233
|
-
nl()
|
|
234
|
-
print(f" {BLUE}{BOLD} ▶ Linking {n_install} package(s) into virtual environment …{RESET}")
|
|
235
|
-
|
|
236
|
-
elif line.startswith("Successfully installed"):
|
|
237
|
-
tail = line.replace("Successfully installed", "").strip()
|
|
238
|
-
count = len(tail.split())
|
|
239
|
-
nl()
|
|
240
|
-
ok(f"{count} package(s) installed successfully")
|
|
241
|
-
if n_cached:
|
|
242
|
-
info(f"{n_cached} package(s) already satisfied (cached)")
|
|
243
|
-
|
|
244
|
-
elif line.upper().startswith("WARNING") or line.upper().startswith("DEPRECATION"):
|
|
245
|
-
pass # suppress pip noise
|
|
246
|
-
|
|
247
|
-
else:
|
|
248
|
-
# Any other line (build output, etc.) show dimmed
|
|
249
|
-
if line.strip():
|
|
250
|
-
print(f" {DIM}{line.strip()}{RESET}")
|
|
251
|
-
|
|
252
|
-
proc.wait()
|
|
253
|
-
if proc.returncode != 0:
|
|
254
|
-
raise subprocess.CalledProcessError(proc.returncode, cmd)
|
|
228
|
+
h2("Installing core runtime packages …")
|
|
229
|
+
info("Optional packages such as local Whisper, OpenCV, and music ID are installed only when enabled.")
|
|
230
|
+
run([str(VENV_PIP), "install", "--progress-bar", "off", *CORE_PACKAGES])
|
|
231
|
+
ok(f"Installed {len(CORE_PACKAGES)} core package(s)")
|
|
255
232
|
|
|
256
233
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
257
234
|
# ── API key validators ────────────────────────────────────────────────────────
|
|
@@ -431,6 +408,9 @@ OLLAMA_MODEL = "qwen3-vl:4b" # vision-language model, fits ~6 GB VRAM / ~8 GB
|
|
|
431
408
|
def setup_ollama():
|
|
432
409
|
h1("Step 4 of 7 — Offline AI Model (Ollama)")
|
|
433
410
|
|
|
411
|
+
keys = _load_saved_api_keys()
|
|
412
|
+
has_cloud_key = any(keys.get(k) for k in ("GEMINI_API_KEY", "GROQ_API_KEY", "OPENROUTER_API_KEY"))
|
|
413
|
+
|
|
434
414
|
print(f"""
|
|
435
415
|
Ollama runs AI models {BOLD}locally on your machine{RESET} — no internet or API
|
|
436
416
|
key required. SuperBrain uses it as a last-resort fallback if all
|
|
@@ -441,7 +421,10 @@ def setup_ollama():
|
|
|
441
421
|
Other options: llama3.2:3b (2 GB / 4 GB RAM), gemma2:2b (1.5 GB / 4 GB RAM)
|
|
442
422
|
""")
|
|
443
423
|
|
|
444
|
-
if
|
|
424
|
+
if has_cloud_key:
|
|
425
|
+
info("Cloud API key(s) detected — Ollama is optional and skipped by default.")
|
|
426
|
+
|
|
427
|
+
if not ask_yn("Set up Ollama offline model?", default=not has_cloud_key):
|
|
445
428
|
warn("Skipping Ollama. Cloud providers only — make sure you have API keys.")
|
|
446
429
|
return
|
|
447
430
|
|
|
@@ -562,6 +545,9 @@ WHISPER_MODELS = {
|
|
|
562
545
|
def setup_whisper():
|
|
563
546
|
h1("Step 5 of 7 — Offline Audio Transcription (Whisper)")
|
|
564
547
|
|
|
548
|
+
keys = _load_saved_api_keys()
|
|
549
|
+
has_groq_key = bool(keys.get("GROQ_API_KEY") or os.getenv("GROQ_API_KEY"))
|
|
550
|
+
|
|
565
551
|
print(f"""
|
|
566
552
|
OpenAI Whisper transcribes audio and video {BOLD}entirely on your machine{RESET}.
|
|
567
553
|
SuperBrain uses it to extract speech from Instagram Reels, YouTube
|
|
@@ -571,6 +557,16 @@ def setup_whisper():
|
|
|
571
557
|
It also pre-downloads a speech model the first time it runs.
|
|
572
558
|
""")
|
|
573
559
|
|
|
560
|
+
if has_groq_key:
|
|
561
|
+
info("Groq API key detected — cloud Whisper is available, so local Whisper is optional.")
|
|
562
|
+
if not ask_yn("Also install local Whisper fallback?", default=False):
|
|
563
|
+
warn("Skipping local Whisper. Groq Whisper will be used when Groq is available.")
|
|
564
|
+
return
|
|
565
|
+
else:
|
|
566
|
+
if not ask_yn("Set up local Whisper offline transcription?", default=True):
|
|
567
|
+
warn("Skipping Whisper setup. Audio transcription will rely on cloud providers if available.")
|
|
568
|
+
return
|
|
569
|
+
|
|
574
570
|
# ── ffmpeg check ──────────────────────────────────────────────────────────
|
|
575
571
|
if shutil.which("ffmpeg"):
|
|
576
572
|
ok("ffmpeg is installed")
|
|
@@ -597,7 +593,7 @@ def setup_whisper():
|
|
|
597
593
|
warn("openai-whisper not found — installing now …")
|
|
598
594
|
nl()
|
|
599
595
|
try:
|
|
600
|
-
cmd = [str(VENV_PIP), "install", "--progress-bar", "off", "openai-whisper"]
|
|
596
|
+
cmd = [str(VENV_PIP), "install", "--progress-bar", "off", "openai-whisper>=20231117"]
|
|
601
597
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
602
598
|
text=True, bufsize=1)
|
|
603
599
|
for raw in proc.stdout: # type: ignore[union-attr]
|
|
@@ -998,80 +994,108 @@ def _detect_local_ip() -> str:
|
|
|
998
994
|
return "127.0.0.1"
|
|
999
995
|
|
|
1000
996
|
def _display_connect_qr(url: str, token: str):
|
|
1001
|
-
"""Display a
|
|
997
|
+
"""Display a QR code in the terminal without depending on the active interpreter."""
|
|
998
|
+
|
|
999
|
+
payload = json.dumps({"url": url, "token": token}, separators=(',', ':'))
|
|
1000
|
+
qr_script = textwrap.dedent(r'''
|
|
1001
|
+
import json
|
|
1002
|
+
import sys
|
|
1002
1003
|
|
|
1003
|
-
The QR encodes a JSON string: {"url": "...", "token": "..."}
|
|
1004
|
-
which the mobile app's QR scanner can read to auto-configure connection.
|
|
1005
|
-
"""
|
|
1006
|
-
try:
|
|
1007
1004
|
import segno
|
|
1008
|
-
except ImportError:
|
|
1009
|
-
# segno not installed — try installing it on the fly
|
|
1010
|
-
try:
|
|
1011
|
-
info("Installing segno for QR code display …")
|
|
1012
|
-
run_q([str(VENV_PIP), "install", "--quiet", "segno"])
|
|
1013
|
-
import segno
|
|
1014
|
-
except Exception:
|
|
1015
|
-
warn("Could not generate QR code (segno not available).")
|
|
1016
|
-
info("Install it: pip install segno")
|
|
1017
|
-
return
|
|
1018
1005
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1006
|
+
payload = sys.argv[1]
|
|
1007
|
+
qr = segno.make(payload, error='L')
|
|
1008
|
+
|
|
1009
|
+
matrix = [list(row) for row in qr.matrix]
|
|
1010
|
+
rows = len(matrix)
|
|
1011
|
+
cols = len(matrix[0]) if rows else 0
|
|
1012
|
+
|
|
1013
|
+
quiet = 2
|
|
1014
|
+
padded_cols = cols + quiet * 2
|
|
1015
|
+
padded_rows = rows + quiet * 2
|
|
1016
|
+
padded = []
|
|
1017
|
+
empty_row = [0] * padded_cols
|
|
1018
|
+
for _ in range(quiet):
|
|
1019
|
+
padded.append(list(empty_row))
|
|
1020
|
+
for row in matrix:
|
|
1021
|
+
padded.append([0] * quiet + row + [0] * quiet)
|
|
1022
|
+
for _ in range(quiet):
|
|
1023
|
+
padded.append(list(empty_row))
|
|
1024
|
+
|
|
1025
|
+
print(f" ┌{'─' * (padded_cols + 4)}┐")
|
|
1026
|
+
|
|
1027
|
+
# Center the title within the inner frame
|
|
1028
|
+
inner_width = padded_cols + 4
|
|
1029
|
+
title = "Scan with SuperBrain App"
|
|
1030
|
+
# If the width is too small, truncate it just in case, though QR is usually wider
|
|
1031
|
+
if len(title) > inner_width - 2:
|
|
1032
|
+
title = title[:inner_width - 2]
|
|
1033
|
+
|
|
1034
|
+
print(f" │{title.center(inner_width)}│")
|
|
1035
|
+
print(f" ├{'─' * inner_width}┤")
|
|
1036
|
+
|
|
1037
|
+
for y in range(0, padded_rows, 2):
|
|
1038
|
+
line_chars = []
|
|
1039
|
+
for x in range(padded_cols):
|
|
1040
|
+
top = padded[y][x] if y < padded_rows else 0
|
|
1041
|
+
bottom = padded[y + 1][x] if y + 1 < padded_rows else 0
|
|
1042
|
+
|
|
1043
|
+
if top and bottom:
|
|
1044
|
+
line_chars.append("█")
|
|
1045
|
+
elif top and not bottom:
|
|
1046
|
+
line_chars.append("▀")
|
|
1047
|
+
elif not top and bottom:
|
|
1048
|
+
line_chars.append("▄")
|
|
1049
|
+
else:
|
|
1050
|
+
line_chars.append(" ")
|
|
1049
1051
|
|
|
1050
|
-
|
|
1051
|
-
print(f" {BOLD}{CYAN}┌{'─' * (padded_cols + 4)}┐{RESET}")
|
|
1052
|
-
print(f" {BOLD}{CYAN}│{RESET} {BOLD}Scan with SuperBrain app{RESET} {BOLD}{CYAN}│{RESET}")
|
|
1053
|
-
print(f" {BOLD}{CYAN}├{'─' * (padded_cols + 4)}┤{RESET}")
|
|
1054
|
-
|
|
1055
|
-
for y in range(0, padded_rows, 2):
|
|
1056
|
-
line_chars = []
|
|
1057
|
-
for x in range(padded_cols):
|
|
1058
|
-
top = padded[y][x] if y < padded_rows else 0
|
|
1059
|
-
bottom = padded[y + 1][x] if y + 1 < padded_rows else 0
|
|
1060
|
-
|
|
1061
|
-
if top and bottom:
|
|
1062
|
-
line_chars.append("█")
|
|
1063
|
-
elif top and not bottom:
|
|
1064
|
-
line_chars.append("▀")
|
|
1065
|
-
elif not top and bottom:
|
|
1066
|
-
line_chars.append("▄")
|
|
1067
|
-
else:
|
|
1068
|
-
line_chars.append(" ")
|
|
1052
|
+
print(f" │ {''.join(line_chars)} │")
|
|
1069
1053
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1054
|
+
print(f" └{'─' * (padded_cols + 4)}┘")
|
|
1055
|
+
''')
|
|
1072
1056
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1057
|
+
interpreters = []
|
|
1058
|
+
if VENV_PYTHON.exists():
|
|
1059
|
+
interpreters.append(str(VENV_PYTHON))
|
|
1060
|
+
if sys.executable not in interpreters:
|
|
1061
|
+
interpreters.append(sys.executable)
|
|
1062
|
+
|
|
1063
|
+
import os
|
|
1064
|
+
env = os.environ.copy()
|
|
1065
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
1066
|
+
|
|
1067
|
+
for python_executable in interpreters:
|
|
1068
|
+
try:
|
|
1069
|
+
result = subprocess.run(
|
|
1070
|
+
[python_executable, "-c", qr_script, payload],
|
|
1071
|
+
check=True,
|
|
1072
|
+
capture_output=True,
|
|
1073
|
+
text=True,
|
|
1074
|
+
encoding="utf-8",
|
|
1075
|
+
errors="replace",
|
|
1076
|
+
env=env,
|
|
1077
|
+
)
|
|
1078
|
+
nl()
|
|
1079
|
+
print(result.stdout, end="")
|
|
1080
|
+
nl()
|
|
1081
|
+
return
|
|
1082
|
+
except subprocess.CalledProcessError as e:
|
|
1083
|
+
try:
|
|
1084
|
+
run([python_executable, "-m", "pip", "install", "--quiet", "segno"])
|
|
1085
|
+
result = run_q([python_executable, "-c", qr_script, payload], encoding="utf-8", errors="replace", env=env)
|
|
1086
|
+
nl()
|
|
1087
|
+
print(result.stdout, end="")
|
|
1088
|
+
nl()
|
|
1089
|
+
return
|
|
1090
|
+
except subprocess.CalledProcessError as e2:
|
|
1091
|
+
warn(f"Failed to generate QR code using {python_executable}. STDOUT: {e2.stdout} STDERR: {e2.stderr}")
|
|
1092
|
+
continue
|
|
1093
|
+
except Exception as ex:
|
|
1094
|
+
warn(f"Failed to generate QR code using {python_executable}: {ex}")
|
|
1095
|
+
continue
|
|
1096
|
+
|
|
1097
|
+
warn("Could not generate QR code.")
|
|
1098
|
+
info("Use the server URL and Access Token shown below to connect manually.")
|
|
1075
1099
|
|
|
1076
1100
|
|
|
1077
1101
|
def launch_backend():
|
|
@@ -1197,7 +1221,7 @@ def launch_backend():
|
|
|
1197
1221
|
tunnel_hint = f" · public → install Node.js first, then run: {DIM}npx localtunnel --port {PORT}{RESET}"
|
|
1198
1222
|
|
|
1199
1223
|
# ── Generate and display QR code ──────────────────────────────────────────
|
|
1200
|
-
qr_url = f"http://{local_ip}:{PORT}"
|
|
1224
|
+
qr_url = localtunnel_url if localtunnel_url else f"http://{local_ip}:{PORT}"
|
|
1201
1225
|
_display_connect_qr(qr_url, token)
|
|
1202
1226
|
|
|
1203
1227
|
print(f"""
|