superbrain-server 1.0.2-beta.0 → 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 ADDED
@@ -0,0 +1,125 @@
1
+ # superbrain-server
2
+
3
+ One-command installer and launcher for the SuperBrain backend.
4
+
5
+ Run the backend on any machine without cloning the repository.
6
+
7
+ ## Install and Run
8
+
9
+ ### Recommended (no global install)
10
+
11
+ ```bash
12
+ npx -y superbrain-server@latest
13
+ ```
14
+
15
+ ### Global install (optional)
16
+
17
+ ```bash
18
+ npm install -g superbrain-server
19
+ superbrain-server
20
+ ```
21
+
22
+ ### Beta channel
23
+
24
+ ```bash
25
+ npx -y superbrain-server@beta
26
+ ```
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
+
46
+ ## What It Does on First Run
47
+
48
+ 1. Unpacks backend files into `~/.superbrain-server`
49
+ 2. Creates an isolated Python virtual environment
50
+ 3. Installs Python dependencies
51
+ 4. Runs interactive setup (AI keys, optional Instagram, token)
52
+ 5. Starts the backend API server
53
+
54
+ ## Requirements
55
+
56
+ - Node.js 20+
57
+ - Python 3.10+
58
+ - ffmpeg
59
+
60
+ ## Commands
61
+
62
+ ```bash
63
+ # Start server
64
+ superbrain-server
65
+
66
+ # Open interactive reset menu
67
+ superbrain-server reset
68
+
69
+ # Full reset (destructive)
70
+ superbrain-server reset --all
71
+ ```
72
+
73
+ You can run the same with npx:
74
+
75
+ ```bash
76
+ npx -y superbrain-server@latest reset
77
+ npx -y superbrain-server@latest reset --all
78
+ ```
79
+
80
+ ## Default Runtime Location
81
+
82
+ The backend is installed under your user home directory:
83
+
84
+ - Windows: `%USERPROFILE%/.superbrain-server`
85
+ - macOS/Linux: `~/.superbrain-server`
86
+
87
+ ## Connect Mobile App
88
+
89
+ After backend starts:
90
+
91
+ 1. Copy the Access Token shown in backend console
92
+ 2. Open SuperBrain app Settings
93
+ 3. Enter server URL and Access Token
94
+
95
+ ## Troubleshooting
96
+
97
+ ### Python not found
98
+
99
+ Install Python 3.10+ and verify:
100
+
101
+ ```bash
102
+ python --version
103
+ ```
104
+
105
+ On Windows, `py -3 --version` should also work.
106
+
107
+ ### Backend not reachable from phone
108
+
109
+ Expose local port with ngrok:
110
+
111
+ ```bash
112
+ ngrok http 5000
113
+ ```
114
+
115
+ Use the generated HTTPS URL in app Settings.
116
+
117
+ ## Links
118
+
119
+ - GitHub repository: https://github.com/sidinsearch/superbrain
120
+ - Main project docs: https://github.com/sidinsearch/superbrain#readme
121
+ - npm package page: https://www.npmjs.com/package/superbrain-server
122
+
123
+ ## License
124
+
125
+ MIT (CLI wrapper)
package/package.json CHANGED
@@ -1,23 +1,25 @@
1
- {
2
- "name": "superbrain-server",
3
- "version": "1.0.2-beta.0",
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
- "author": "sidinsearch",
17
- "license": "MIT",
18
- "dependencies": {},
19
- "files": [
20
- "bin/",
21
- "payload/"
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(..., description="Access Token for authentication")):
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
- if x_api_key != API_TOKEN:
80
- logger.warning("Invalid Access Token attempt from IP: %s", request.client.host if hasattr(request, 'client') else 'unknown')
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 x_api_key
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.3587389144652606,
5
- "success_count": 48,
4
+ "avg_response_s": 1.325513668372554,
5
+ "success_count": 50,
6
6
  "fail_count": 1,
7
7
  "down_until": null,
8
- "last_used": "2026-02-28T15:46:20.843218",
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": 9,
327
- "down_until": "2026-04-01T11:15:39.217625",
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": 12,
337
- "down_until": "2026-04-01T11:15:39.214310",
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": 11,
357
- "down_until": "2026-04-01T11:15:39.217625",
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": 10,
367
- "down_until": "2026-04-01T11:15:39.217625",
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": 9,
377
- "down_until": "2026-04-01T11:15:39.220783",
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": 9,
387
- "down_until": "2026-04-01T11:15:39.220783",
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": 9,
397
- "down_until": "2026-04-01T11:15:39.220783",
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": 2.1663871744251377,
405
- "success_count": 28,
404
+ "avg_response_s": 1.9110158948201241,
405
+ "success_count": 30,
406
406
  "fail_count": 0,
407
407
  "down_until": null,
408
- "last_used": "2026-02-28T15:46:16.547477",
408
+ "last_used": "2026-04-06T17:49:07.508119",
409
409
  "last_error": null,
410
410
  "base_priority": 5
411
411
  },
@@ -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
- try:
130
- importlib.import_module(module_name)
131
- except Exception:
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 packages from requirements.txt …")
193
- nl()
194
-
195
- # ── stream pip output and display each package live ────────────────────────
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 not ask_yn("Set up Ollama offline model?", default=True):
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 proper QR code in the terminal using segno + Unicode half-block chars.
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
- payload = json.dumps({"url": url, "token": token}, separators=(',', ':'))
1020
- qr = segno.make(payload, error='L')
1021
-
1022
- # Convert to a matrix of booleans (True = dark module)
1023
- matrix = [list(row) for row in qr.matrix]
1024
- rows = len(matrix)
1025
- cols = len(matrix[0]) if rows else 0
1026
-
1027
- # Add quiet zone (2 modules on each side)
1028
- quiet = 2
1029
- padded_cols = cols + quiet * 2
1030
- padded_rows = rows + quiet * 2
1031
- padded = []
1032
- empty_row = [0] * padded_cols
1033
- for _ in range(quiet):
1034
- padded.append(list(empty_row))
1035
- for row in matrix:
1036
- padded.append([0] * quiet + row + [0] * quiet)
1037
- for _ in range(quiet):
1038
- padded.append(list(empty_row))
1039
-
1040
- # Render using Unicode half-block characters for double vertical resolution
1041
- # Each output line encodes TWO rows of QR modules:
1042
- # top=dark, bottom=dark "█" (full block)
1043
- # top=dark, bottom=light "▀" (upper half)
1044
- # top=light, bottom=dark "▄" (lower half)
1045
- # top=light, bottom=light " " (space)
1046
- BG_WHITE = "\033[47m" # white background
1047
- FG_BLACK = "\033[30m" # black foreground
1048
- ANSI_RST = "\033[0m"
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
- nl()
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
- line = "".join(line_chars)
1071
- print(f" {BOLD}{CYAN}│{RESET} {line} {BOLD}{CYAN}│{RESET}")
1054
+ print(f" └{'─' * (padded_cols + 4)}┘")
1055
+ ''')
1072
1056
 
1073
- print(f" {BOLD}{CYAN}└{'─' * (padded_cols + 4)}┘{RESET}")
1074
- nl()
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"""