superbrain-server 1.0.17 → 1.0.19

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/payload/start.py +207 -212
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superbrain-server",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "1-Line Auto-Installer and Server Execution wrapper for SuperBrain",
5
5
  "main": "index.js",
6
6
  "bin": {
package/payload/start.py CHANGED
@@ -1,14 +1,14 @@
1
- #!/usr/bin/env python3
1
+ #!/usr/bin/env python3
2
2
  """
3
- ╔══════════════════════════════════════════════════════════════════╗
4
- ║ SuperBrain — First-Time Setup & Launcher ║
5
- â•‘ Run this once to configure everything, then again â•‘
6
- â•‘ any time to start the server. â•‘
7
- ╚══════════════════════════════════════════════════════════════════╝
3
+ ╔══════════════════════════════════════════════════════════════════╗
4
+ SuperBrain First-Time Setup & Launcher
5
+ Run this once to configure everything, then again
6
+ any time to start the server.
7
+ ╚══════════════════════════════════════════════════════════════════╝
8
8
 
9
9
  Usage:
10
- python start.py — interactive setup on first run, then start server
11
- python start.py --reset — re-run the full setup wizard
10
+ python start.py interactive setup on first run, then start server
11
+ python start.py --reset re-run the full setup wizard
12
12
  """
13
13
 
14
14
  import sys
@@ -25,7 +25,7 @@ import importlib
25
25
  import re
26
26
  from pathlib import Path
27
27
 
28
- # ── Paths ─────────────────────────────────────────────────────────────────────
28
+ # ── Paths ─────────────────────────────────────────────────────────────────────
29
29
  BASE_DIR = Path(__file__).parent.resolve()
30
30
  VENV_DIR = BASE_DIR / ".venv"
31
31
  API_KEYS = BASE_DIR / "config" / ".api_keys"
@@ -37,7 +37,7 @@ PYTHON = sys.executable # path that launched this script
37
37
  VENV_PYTHON = (VENV_DIR / "Scripts" / "python.exe") if IS_WINDOWS else (VENV_DIR / "bin" / "python")
38
38
  VENV_PIP = (VENV_DIR / "Scripts" / "pip.exe") if IS_WINDOWS else (VENV_DIR / "bin" / "pip")
39
39
 
40
- # ── ANSI colours (stripped on Windows unless ANSICON / Windows Terminal) ──────
40
+ # ── ANSI colours (stripped on Windows unless ANSICON / Windows Terminal) ──────
41
41
  def _ansi(code): return f"\033[{code}m"
42
42
  RESET = _ansi(0); BOLD = _ansi(1)
43
43
  RED = _ansi(31); GREEN = _ansi(32); YELLOW = _ansi(33)
@@ -47,48 +47,48 @@ MAGENTA = _ansi(35)
47
47
  MAG = MAGENTA
48
48
 
49
49
  def link(url: str, text: str | None = None) -> str:
50
- """OSC 8 terminal hyperlink — clickable in most modern terminals."""
50
+ """OSC 8 terminal hyperlink clickable in most modern terminals."""
51
51
  label = text or url
52
52
  return f"\033]8;;{url}\033\\{label}\033]8;;\033\\"
53
53
 
54
54
  def banner():
55
55
  art = f"""{CYAN}{BOLD}
56
- ███████╗██╗ ██╗██████╗ ███████╗██████╗
57
- ██╔════╝██║ ██║██╔══██╗██╔════╝██╔══██╗
58
- ███████╗██║ ██║██████╔╝█████╗ ██████╔╝
59
- ╚════██║██║ ██║██╔═══╝ ██╔══╝ ██╔══██╗
60
- ███████║╚██████╔╝██║ ███████╗██║ ██║
61
- ╚══════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
62
-
63
- ██████╗ ██████╗ █████╗ ██╗███╗ ██╗
64
- ██╔══██╗██╔══██╗██╔══██╗██║████╗ ██║
65
- ██████╔╝██████╔╝███████║██║██╔██╗ ██║
66
- ██╔══██╗██╔══██╗██╔══██║██║██║╚██╗██║
67
- ██████╔╝██║ ██║██║ ██║██║██║ ╚████║
68
- ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝
56
+ ███████╗██╗ ██╗██████╗ ███████╗██████╗
57
+ ██╔════╝██║ ██║██╔══██╗██╔════╝██╔══██╗
58
+ ███████╗██║ ██║██████╔╝█████╗ ██████╔╝
59
+ ╚════██║██║ ██║██╔═══╝ ██╔══╝ ██╔══██╗
60
+ ███████║╚██████╔╝██║ ███████╗██║ ██║
61
+ ╚══════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
62
+
63
+ ██████╗ ██████╗ █████╗ ██╗███╗ ██╗
64
+ ██╔══██╗██╔══██╗██╔══██╗██║████╗ ██║
65
+ ██████╔╝██████╔╝███████║██║██╔██╗ ██║
66
+ ██╔══██╗██╔══██╗██╔══██║██║██║╚██╗██║
67
+ ██████╔╝██║ ██║██║ ██║██║██║ ╚████║
68
+ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝
69
69
  {RESET}"""
70
- credit = (f" {DIM}made with {RESET}{MAG}❤{RESET}{DIM} by "
70
+ credit = (f" {DIM}made with {RESET}{MAG}{RESET}{DIM} by "
71
71
  f"{link('https://github.com/sidinsearch', f'{BOLD}sidinsearch{RESET}{DIM}')}"
72
72
  f"{RESET}\n")
73
73
  print(art + credit)
74
74
 
75
- def h1(msg): print(f"\n{BOLD}{CYAN}{'━'*64}{RESET}\n{BOLD} {msg}{RESET}\n{BOLD}{CYAN}{'━'*64}{RESET}")
76
- def h2(msg): print(f"\n{BOLD}{BLUE} â–¶ {msg}{RESET}")
77
- def ok(msg): print(f" {GREEN}✓{RESET} {msg}")
78
- def warn(msg):print(f" {YELLOW}âš {RESET} {msg}")
79
- def err(msg): print(f" {RED}✗{RESET} {msg}")
75
+ def h1(msg): print(f"\n{BOLD}{CYAN}{''*64}{RESET}\n{BOLD} {msg}{RESET}\n{BOLD}{CYAN}{''*64}{RESET}")
76
+ def h2(msg): print(f"\n{BOLD}{BLUE} {msg}{RESET}")
77
+ def ok(msg): print(f" {GREEN}{RESET} {msg}")
78
+ def warn(msg):print(f" {YELLOW}{RESET} {msg}")
79
+ def err(msg): print(f" {RED}{RESET} {msg}")
80
80
  def info(msg):print(f" {DIM}{msg}{RESET}")
81
81
  def nl(): print()
82
82
 
83
83
  def ask(prompt, default=None, secret=False, paste=False):
84
84
  """
85
85
  Prompt for input.
86
- secret=True — uses getpass (hidden, no echo) — good for passwords typed char-by-char.
87
- paste=True — uses plain input (visible) so Ctrl+V / right-click paste works;
88
- existing value is shown as ●●●● to indicate something is already set.
86
+ secret=True uses getpass (hidden, no echo) good for passwords typed char-by-char.
87
+ paste=True uses plain input (visible) so Ctrl+V / right-click paste works;
88
+ existing value is shown as ●●●● to indicate something is already set.
89
89
  """
90
90
  if paste and default:
91
- display_default = f" [{DIM}●●●● (already set — paste to replace){RESET}]"
91
+ display_default = f" [{DIM}●●●● (already set paste to replace){RESET}]"
92
92
  elif default:
93
93
  display_default = f" [{DIM}{default}{RESET}]"
94
94
  else:
@@ -177,7 +177,7 @@ def ensure_runtime_dependencies():
177
177
  return
178
178
 
179
179
  warn(f"Missing runtime package(s): {', '.join(missing)}")
180
- info("Installing missing runtime package(s) automatically …")
180
+ info("Installing missing runtime package(s) automatically ")
181
181
  try:
182
182
  run([str(VENV_PYTHON), "-m", "pip", "install", *missing])
183
183
  ok("Runtime dependencies installed")
@@ -186,7 +186,7 @@ def ensure_runtime_dependencies():
186
186
  info(f"Run manually: {VENV_PYTHON} -m pip install {' '.join(missing)}")
187
187
  sys.exit(1)
188
188
 
189
- # ── Helpers for live output displays ───────────────────────────────────────────────
189
+ # ── Helpers for live output displays ───────────────────────────────────────────────
190
190
  BAR_WIDTH = 36
191
191
 
192
192
  def _ascii_bar(completed: int, total: int, width: int = BAR_WIDTH) -> str:
@@ -195,7 +195,7 @@ def _ascii_bar(completed: int, total: int, width: int = BAR_WIDTH) -> str:
195
195
  return ""
196
196
  pct = min(completed / total, 1.0)
197
197
  fill = int(width * pct)
198
- bar = f"{GREEN}{'â–ˆ' * fill}{DIM}{'â–‘' * (width - fill)}{RESET}"
198
+ bar = f"{GREEN}{'' * fill}{DIM}{'' * (width - fill)}{RESET}"
199
199
  mb_d = completed / 1_048_576
200
200
  mb_t = total / 1_048_576
201
201
  return f"[{bar}] {mb_d:6.1f} / {mb_t:.1f} MB {pct*100:5.1f}%"
@@ -205,42 +205,42 @@ def _overwrite(line: str):
205
205
  sys.stdout.write(f"\r {line}")
206
206
  sys.stdout.flush()
207
207
 
208
- # ══════════════════════════════════════════════════════════════════════════════
209
- # Step 1 — Virtual Environment
210
- # ══════════════════════════════════════════════════════════════════════════════
208
+ # ══════════════════════════════════════════════════════════════════════════════
209
+ # Step 1 Virtual Environment
210
+ # ══════════════════════════════════════════════════════════════════════════════
211
211
  def setup_venv():
212
- h1("Step 1 of 6 — Python Virtual Environment")
212
+ h1("Step 1 of 6 Python Virtual Environment")
213
213
  if VENV_DIR.exists():
214
214
  ok(f"Virtual environment already exists at {VENV_DIR}")
215
215
  return
216
- h2("Creating virtual environment …")
216
+ h2("Creating virtual environment ")
217
217
  run([PYTHON, "-m", "venv", str(VENV_DIR)])
218
218
  ok(f"Virtual environment created at {VENV_DIR}")
219
219
 
220
- # ══════════════════════════════════════════════════════════════════════════════
221
- # Step 2 — Install Dependencies
222
- # ══════════════════════════════════════════════════════════════════════════════
220
+ # ══════════════════════════════════════════════════════════════════════════════
221
+ # Step 2 Install Dependencies
222
+ # ══════════════════════════════════════════════════════════════════════════════
223
223
  def install_deps():
224
- h1("Step 2 of 7 — Installing Core Python Dependencies")
224
+ h1("Step 2 of 7 Installing Core Python Dependencies")
225
225
 
226
- h2("Upgrading pip …")
226
+ h2("Upgrading pip ")
227
227
  run([str(VENV_PYTHON), "-m", "pip", "install", "--quiet", "--upgrade", "pip"])
228
228
  ok("pip up to date")
229
229
 
230
- h2("Installing core runtime packages …")
230
+ h2("Installing core runtime packages ")
231
231
  info("Optional packages such as local Whisper, OpenCV, and music ID are installed only when enabled.")
232
232
  run([str(VENV_PIP), "install", "--progress-bar", "off", *CORE_PACKAGES])
233
233
  ok(f"Installed {len(CORE_PACKAGES)} core package(s)")
234
234
 
235
- # ══════════════════════════════════════════════════════════════════════════════
236
- # ── API key validators ────────────────────────────────────────────────────────
235
+ # ══════════════════════════════════════════════════════════════════════════════
236
+ # ── API key validators ────────────────────────────────────────────────────────
237
237
  # Return values:
238
- # (True, detail) — key is definitely valid
239
- # (False, detail) — key is definitely INVALID (401 / explicit auth error)
240
- # (None, detail) — could not verify (network, 403 scope, timeout, etc.)
238
+ # (True, detail) key is definitely valid
239
+ # (False, detail) key is definitely INVALID (401 / explicit auth error)
240
+ # (None, detail) could not verify (network, 403 scope, timeout, etc.)
241
241
 
242
242
  def _validate_gemini(key: str):
243
- """Hit the Gemini models list endpoint — any valid key returns 200."""
243
+ """Hit the Gemini models list endpoint any valid key returns 200."""
244
244
  try:
245
245
  import urllib.request as _r, json as _j
246
246
  req = _r.Request(
@@ -256,7 +256,7 @@ def _validate_gemini(key: str):
256
256
  return False, "invalid key (400 Bad Request)"
257
257
  if "401" in msg:
258
258
  return False, "invalid key (401 Unauthorized)"
259
- # 403, timeouts, etc. — cannot determine validity
259
+ # 403, timeouts, etc. cannot determine validity
260
260
  return None, f"could not verify ({msg[:70]})"
261
261
 
262
262
  def _validate_groq(key: str):
@@ -285,7 +285,7 @@ def _validate_groq(key: str):
285
285
  except _e.HTTPError as e:
286
286
  if e.code in (401, 400):
287
287
  return False, f"invalid key ({e.code} {e.reason})"
288
- # 403, 429, 503, etc. — key may be fine
288
+ # 403, 429, 503, etc. key may be fine
289
289
  return None, f"could not verify ({e.code} {e.reason})"
290
290
  except Exception as e:
291
291
  return None, f"could not verify ({str(e)[:70]})"
@@ -312,38 +312,38 @@ def _check_and_report(name: str, key: str, validator) -> str:
312
312
  """Validate `key`, print result inline, return the key unchanged."""
313
313
  if not key:
314
314
  return key
315
- print(f" {DIM}Checking {name} key …{RESET}", end="", flush=True)
315
+ print(f" {DIM}Checking {name} key {RESET}", end="", flush=True)
316
316
  result, detail = validator(key)
317
317
  if result is True:
318
- print(f"\r {GREEN}✓{RESET} {name}: {detail} ")
318
+ print(f"\r {GREEN}{RESET} {name}: {detail} ")
319
319
  elif result is False:
320
- print(f"\r {RED}✗{RESET} {name}: {detail} ")
321
- warn(f"That key looks invalid — double-check at the provider dashboard.")
320
+ print(f"\r {RED}{RESET} {name}: {detail} ")
321
+ warn(f"That key looks invalid double-check at the provider dashboard.")
322
322
  else:
323
- # None — ambiguous, don't cry wolf
323
+ # None ambiguous, don't cry wolf
324
324
  print(f"\r {YELLOW}~{RESET} {name}: {detail} ")
325
- info("Could not reach the API right now — key saved, will be tested on first use.")
325
+ info("Could not reach the API right now key saved, will be tested on first use.")
326
326
  return key
327
327
 
328
- # Step 3 — API Keys
329
- # ══════════════════════════════════════════════════════════════════════════════
328
+ # Step 3 API Keys
329
+ # ══════════════════════════════════════════════════════════════════════════════
330
330
  def setup_api_keys():
331
- h1("Step 3 of 7 — AI Provider Keys")
331
+ h1("Step 3 of 7 AI Provider Keys")
332
332
 
333
333
  print(f"""
334
334
  SuperBrain uses AI providers to analyse your saved content.
335
- You need {BOLD}at least one{RESET} key — the router tries them in order and
335
+ You need {BOLD}at least one{RESET} key the router tries them in order and
336
336
  falls back automatically.
337
337
 
338
- Recommended: {GREEN}Gemini{RESET} (most generous free tier — 1 500 req/day)
338
+ Recommended: {GREEN}Gemini{RESET} (most generous free tier 1 500 req/day)
339
339
 
340
340
  Get free keys:
341
- Gemini → {CYAN}https://aistudio.google.com/apikey{RESET}
342
- Groq → {CYAN}https://console.groq.com/keys{RESET}
343
- OpenRouter → {CYAN}https://openrouter.ai/keys{RESET}
341
+ Gemini {CYAN}https://aistudio.google.com/apikey{RESET}
342
+ Groq {CYAN}https://console.groq.com/keys{RESET}
343
+ OpenRouter {CYAN}https://openrouter.ai/keys{RESET}
344
344
 
345
345
  Press {BOLD}Enter{RESET} to skip any key you don't have yet.
346
- {DIM}Keys and passwords are visible as you paste — don't run setup in a screen share.{RESET}
346
+ {DIM}Keys and passwords are visible as you paste don't run setup in a screen share.{RESET}
347
347
  """)
348
348
 
349
349
  # Load existing values if re-running
@@ -371,14 +371,14 @@ def setup_api_keys():
371
371
  print(f" {BOLD}Instagram Credentials{RESET}")
372
372
  print(f"""
373
373
  Used for downloading private/public Instagram posts.
374
- {YELLOW}Use a secondary / burner account — NOT your main account.{RESET}
374
+ {YELLOW}Use a secondary / burner account NOT your main account.{RESET}
375
375
  The session is cached after first login so you won't be asked again.
376
376
 
377
377
  {DIM}Without credentials:{RESET}
378
378
  SuperBrain can still save and analyse {BOLD}YouTube videos{RESET} and {BOLD}Websites{RESET}
379
379
  without any Instagram account. However, Instagram posts will be limited:
380
- • Only {BOLD}public posts{RESET} that are accessible without login may work.
381
- • You {BOLD}cannot process multiple Instagram posts back-to-back{RESET} —
380
+ Only {BOLD}public posts{RESET} that are accessible without login may work.
381
+ You {BOLD}cannot process multiple Instagram posts back-to-back{RESET}
382
382
  Instagram enforces a rate-limit cool-down between unauthenticated
383
383
  requests. You may need to wait several minutes between saves.
384
384
  Adding credentials removes these restrictions entirely.
@@ -391,7 +391,7 @@ def setup_api_keys():
391
391
  # Write .api_keys
392
392
  API_KEYS.parent.mkdir(parents=True, exist_ok=True)
393
393
  lines = [
394
- "# SuperBrain API Keys — DO NOT COMMIT THIS FILE\n",
394
+ "# SuperBrain API Keys DO NOT COMMIT THIS FILE\n",
395
395
  f"GEMINI_API_KEY={gemini}\n",
396
396
  f"GROQ_API_KEY={groq_k}\n",
397
397
  f"OPENROUTER_API_KEY={openr}\n",
@@ -402,32 +402,32 @@ def setup_api_keys():
402
402
  API_KEYS.write_text("".join(lines))
403
403
  ok(f"Keys saved to {API_KEYS}")
404
404
 
405
- # ══════════════════════════════════════════════════════════════════════════════
406
- # Step 4 — Ollama / Offline Model
407
- # ══════════════════════════════════════════════════════════════════════════════
405
+ # ══════════════════════════════════════════════════════════════════════════════
406
+ # Step 4 Ollama / Offline Model
407
+ # ══════════════════════════════════════════════════════════════════════════════
408
408
  OLLAMA_MODEL = "qwen3-vl:4b" # vision-language model, fits ~6 GB VRAM / ~8 GB RAM
409
409
 
410
410
  def setup_ollama():
411
- h1("Step 4 of 7 — Offline AI Model (Ollama)")
411
+ h1("Step 4 of 7 Offline AI Model (Ollama)")
412
412
 
413
413
  keys = _load_saved_api_keys()
414
414
  has_cloud_key = any(keys.get(k) for k in ("GEMINI_API_KEY", "GROQ_API_KEY", "OPENROUTER_API_KEY"))
415
415
 
416
416
  print(f"""
417
- Ollama runs AI models {BOLD}locally on your machine{RESET} — no internet or API
417
+ Ollama runs AI models {BOLD}locally on your machine{RESET} no internet or API
418
418
  key required. SuperBrain uses it as a last-resort fallback if all
419
419
  cloud providers fail or run out of quota.
420
420
 
421
421
  Recommended model: {BOLD}{OLLAMA_MODEL}{RESET} (~3 GB download, needs ~8 GB RAM)
422
- → Vision-language model: understands both text AND images.
422
+ Vision-language model: understands both text AND images.
423
423
  Other options: llama3.2:3b (2 GB / 4 GB RAM), gemma2:2b (1.5 GB / 4 GB RAM)
424
424
  """)
425
425
 
426
426
  if has_cloud_key:
427
- info("Cloud API key(s) detected — Ollama is optional and skipped by default.")
427
+ info("Cloud API key(s) detected Ollama is optional and skipped by default.")
428
428
 
429
429
  if not ask_yn("Set up Ollama offline model?", default=not has_cloud_key):
430
- warn("Skipping Ollama. Cloud providers only — make sure you have API keys.")
430
+ warn("Skipping Ollama. Cloud providers only make sure you have API keys.")
431
431
  return
432
432
 
433
433
  # Check if ollama binary is available
@@ -436,8 +436,8 @@ def setup_ollama():
436
436
  {YELLOW}Ollama is not installed.{RESET}
437
437
 
438
438
  Install it first:
439
- Linux / macOS → {CYAN}curl -fsSL https://ollama.com/install.sh | sh{RESET}
440
- Windows → Download from {CYAN}https://ollama.com/download{RESET}
439
+ Linux / macOS {CYAN}curl -fsSL https://ollama.com/install.sh | sh{RESET}
440
+ Windows Download from {CYAN}https://ollama.com/download{RESET}
441
441
 
442
442
  After installing, re-run {BOLD}python start.py{RESET} to continue.
443
443
  """)
@@ -460,7 +460,7 @@ def setup_ollama():
460
460
  custom = ask(f"Model to pull", default=OLLAMA_MODEL)
461
461
  model = custom or OLLAMA_MODEL
462
462
 
463
- h2(f"Pulling {model} — this downloads ~3 GB, grab a coffee ☕")
463
+ h2(f"Pulling {model} this downloads ~3 GB, grab a coffee ")
464
464
  nl()
465
465
  try:
466
466
  _ollama_pull_with_progress(model)
@@ -476,7 +476,7 @@ def _ollama_pull_with_progress(model: str):
476
476
  proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
477
477
  text=True, bufsize=1)
478
478
 
479
- # digest → (total_bytes, completed_bytes, short_label)
479
+ # digest (total_bytes, completed_bytes, short_label)
480
480
  layers: dict[str, tuple[int, int, str]] = {}
481
481
  last_status = ""
482
482
  active_digest = ""
@@ -487,7 +487,7 @@ def _ollama_pull_with_progress(model: str):
487
487
  if not raw:
488
488
  continue
489
489
 
490
- # Ollama outputs plain-text lines (not JSON) when not a TTY — accept both
490
+ # Ollama outputs plain-text lines (not JSON) when not a TTY accept both
491
491
  try:
492
492
  data = _json.loads(raw)
493
493
  except _json.JSONDecodeError:
@@ -496,7 +496,7 @@ def _ollama_pull_with_progress(model: str):
496
496
  sys.stdout.write("\n"); render_line = False
497
497
  if raw != last_status:
498
498
  last_status = raw
499
- print(f" {CYAN}→{RESET} {raw}")
499
+ print(f" {CYAN}{RESET} {raw}")
500
500
  continue
501
501
 
502
502
  status = data.get("status", "")
@@ -520,9 +520,9 @@ def _ollama_pull_with_progress(model: str):
520
520
  done_statuses = ("verifying sha256 digest", "writing manifest",
521
521
  "removing any unused layers", "success")
522
522
  if any(s in status.lower() for s in done_statuses):
523
- print(f" {GREEN}✓{RESET} {status}")
523
+ print(f" {GREEN}{RESET} {status}")
524
524
  else:
525
- print(f" {CYAN}→{RESET} {status}")
525
+ print(f" {CYAN}{RESET} {status}")
526
526
 
527
527
  if render_line:
528
528
  sys.stdout.write("\n"); render_line = False
@@ -533,19 +533,19 @@ def _ollama_pull_with_progress(model: str):
533
533
 
534
534
  ok(f"Model {model} ready")
535
535
 
536
- # ══════════════════════════════════════════════════════════════════════════════
537
- # Step 5 — Whisper / Offline Transcription
538
- # ══════════════════════════════════════════════════════════════════════════════
536
+ # ══════════════════════════════════════════════════════════════════════════════
537
+ # Step 5 Whisper / Offline Transcription
538
+ # ══════════════════════════════════════════════════════════════════════════════
539
539
  WHISPER_MODELS = {
540
540
  "tiny": (" ~74 MB", "fastest, lower accuracy"),
541
- "base": ("~142 MB", "good balance ⭐ recommended"),
541
+ "base": ("~142 MB", "good balance recommended"),
542
542
  "small": ("~461 MB", "higher accuracy"),
543
543
  "medium": ("~1.5 GB", "high accuracy, slower"),
544
544
  "large": ("~2.9 GB", "best accuracy, needs 10 GB RAM"),
545
545
  }
546
546
 
547
547
  def setup_whisper():
548
- h1("Step 5 of 7 — Offline Audio Transcription (Whisper)")
548
+ h1("Step 5 of 7 Offline Audio Transcription (Whisper)")
549
549
 
550
550
  keys = _load_saved_api_keys()
551
551
  has_groq_key = bool(keys.get("GROQ_API_KEY") or os.getenv("GROQ_API_KEY"))
@@ -553,14 +553,14 @@ def setup_whisper():
553
553
  print(f"""
554
554
  OpenAI Whisper transcribes audio and video {BOLD}entirely on your machine{RESET}.
555
555
  SuperBrain uses it to extract speech from Instagram Reels, YouTube
556
- videos, and any other saved media — no API key needed.
556
+ videos, and any other saved media no API key needed.
557
557
 
558
558
  Whisper requires {BOLD}ffmpeg{RESET} to be installed on your system.
559
559
  It also pre-downloads a speech model the first time it runs.
560
560
  """)
561
561
 
562
562
  if has_groq_key:
563
- info("Groq API key detected — cloud Whisper is available, so local Whisper is optional.")
563
+ info("Groq API key detected cloud Whisper is available, so local Whisper is optional.")
564
564
  if not ask_yn("Also install local Whisper fallback?", default=False):
565
565
  warn("Skipping local Whisper. Groq Whisper will be used when Groq is available.")
566
566
  return
@@ -569,30 +569,30 @@ def setup_whisper():
569
569
  warn("Skipping Whisper setup. Audio transcription will rely on cloud providers if available.")
570
570
  return
571
571
 
572
- # ── ffmpeg check ──────────────────────────────────────────────────────────
572
+ # ── ffmpeg check ──────────────────────────────────────────────────────────
573
573
  if shutil.which("ffmpeg"):
574
574
  ok("ffmpeg is installed")
575
575
  else:
576
- warn("ffmpeg is NOT installed — Whisper cannot run without it.")
576
+ warn("ffmpeg is NOT installed Whisper cannot run without it.")
577
577
  print(f"""
578
578
  Install ffmpeg:
579
- Linux / WSL → {CYAN}sudo apt install ffmpeg{RESET}
580
- macOS → {CYAN}brew install ffmpeg{RESET}
581
- Windows → {CYAN}winget install ffmpeg{RESET}
579
+ Linux / WSL {CYAN}sudo apt install ffmpeg{RESET}
580
+ macOS {CYAN}brew install ffmpeg{RESET}
581
+ Windows {CYAN}winget install ffmpeg{RESET}
582
582
  or download from {CYAN}https://ffmpeg.org/download.html{RESET}
583
583
 
584
584
  After installing ffmpeg, re-run {BOLD}python start.py --reset{RESET} or just
585
- restart — Whisper will work automatically once ffmpeg is present.
585
+ restart Whisper will work automatically once ffmpeg is present.
586
586
  """)
587
587
  if not ask_yn("Continue setup anyway?", default=True):
588
588
  sys.exit(0)
589
589
 
590
- # ── Whisper package check / install ──────────────────────────────────────
590
+ # ── Whisper package check / install ──────────────────────────────────────
591
591
  try:
592
592
  result = run_q([str(VENV_PYTHON), "-c", "import whisper; print(whisper.__version__)"])
593
593
  ok(f"openai-whisper installed (version {result.stdout.strip()})")
594
594
  except Exception:
595
- warn("openai-whisper not found — installing now …")
595
+ warn("openai-whisper not found installing now ")
596
596
  nl()
597
597
  try:
598
598
  cmd = [str(VENV_PIP), "install", "--progress-bar", "off", "openai-whisper>=20231117"]
@@ -603,13 +603,13 @@ def setup_whisper():
603
603
  if not line:
604
604
  continue
605
605
  if line.startswith("Collecting "):
606
- print(f" {CYAN}↓{RESET} {BOLD}{line.split()[1]}{RESET}")
606
+ print(f" {CYAN}{RESET} {BOLD}{line.split()[1]}{RESET}")
607
607
  elif "Downloading" in line and (".whl" in line or ".tar.gz" in line):
608
608
  parts = line.strip().split()
609
609
  if len(parts) >= 2:
610
- print(f" {DIM}↓ {parts[1]} {' '.join(parts[2:]).strip('()')}{RESET}")
610
+ print(f" {DIM} {parts[1]} {' '.join(parts[2:]).strip('()')}{RESET}")
611
611
  elif line.startswith("Successfully installed"):
612
- print(f" {GREEN}✓ {line}{RESET}")
612
+ print(f" {GREEN} {line}{RESET}")
613
613
  elif "error" in line.lower() or "ERROR" in line:
614
614
  print(f" {RED}{line}{RESET}")
615
615
  proc.wait()
@@ -617,7 +617,7 @@ def setup_whisper():
617
617
  result = run_q([str(VENV_PYTHON), "-c", "import whisper; print(whisper.__version__)"])
618
618
  ok(f"openai-whisper installed (version {result.stdout.strip()})")
619
619
  else:
620
- err("openai-whisper install failed — offline transcription will not work.")
620
+ err("openai-whisper install failed offline transcription will not work.")
621
621
  if not ask_yn("Continue setup anyway?", default=True):
622
622
  sys.exit(0)
623
623
  return
@@ -627,50 +627,50 @@ def setup_whisper():
627
627
  sys.exit(0)
628
628
  return
629
629
 
630
- # ── Model pre-download ────────────────────────────────────────────────────
630
+ # ── Model pre-download ────────────────────────────────────────────────────
631
631
  nl()
632
632
  print(f" {BOLD}Whisper model pre-download{RESET}")
633
633
  print(f" Pre-downloading a model now avoids a delay on first use.\n")
634
634
 
635
635
  rows = ""
636
636
  for name, (size, note) in WHISPER_MODELS.items():
637
- star = f" {YELLOW}← default if skipped{RESET}" if name == "base" else ""
637
+ star = f" {YELLOW} default if skipped{RESET}" if name == "base" else ""
638
638
  rows += f" {BOLD}{name:<8}{RESET} {size} {DIM}{note}{RESET}{star}\n"
639
639
  print(rows)
640
640
 
641
641
  choice = ask("Model to pre-download", default="base")
642
642
  model = choice.strip().lower() if choice else "base"
643
643
  if model not in WHISPER_MODELS:
644
- warn(f"Unknown model '{model}' — defaulting to 'base'.")
644
+ warn(f"Unknown model '{model}' defaulting to 'base'.")
645
645
  model = "base"
646
646
 
647
- # ── Save model choice to config ─────────────────────────────────────────
647
+ # ── Save model choice to config ─────────────────────────────────────────
648
648
  whisper_cfg = BASE_DIR / "config" / "whisper_model.txt"
649
649
  (BASE_DIR / "config").mkdir(exist_ok=True)
650
650
  whisper_cfg.write_text(model)
651
651
  ok(f"Whisper model set to '{model}' (saved to config/whisper_model.txt)")
652
652
 
653
- h2(f"Pre-downloading Whisper '{model}' model …")
653
+ h2(f"Pre-downloading Whisper '{model}' model ")
654
654
  print(f" {DIM}(Whisper's own progress bar will appear below){RESET}\n")
655
655
  try:
656
656
  # Don't capture: let tqdm's download progress bars stream to the terminal
657
657
  run([str(VENV_PYTHON), "-c",
658
- f"import whisper; print('Loading model …'); whisper.load_model('{model}'); print('Done.')"])
658
+ f"import whisper; print('Loading model '); whisper.load_model('{model}'); print('Done.')"])
659
659
  nl()
660
660
  ok(f"Whisper '{model}' model downloaded and cached")
661
661
  except subprocess.CalledProcessError:
662
- err(f"Pre-download failed — Whisper will download '{model}' automatically on first use.")
662
+ err(f"Pre-download failed Whisper will download '{model}' automatically on first use.")
663
663
 
664
- # ══════════════════════════════════════════════════════════════════════════════
665
- # Step 6 — Remote Access / Port Forwarding
666
- # ══════════════════════════════════════════════════════════════════════════════
664
+ # ══════════════════════════════════════════════════════════════════════════════
665
+ # Step 6 Remote Access / Port Forwarding
666
+ # ══════════════════════════════════════════════════════════════════════════════
667
667
  NGROK_ENABLED = BASE_DIR / "config" / "ngrok_enabled.txt"
668
668
  NGROK_TOKEN = BASE_DIR / "config" / "ngrok_token.txt"
669
669
  LOCALTUNNEL_LOG = BASE_DIR / "config" / "localtunnel.log"
670
670
  LOCALTUNNEL_LOG = BASE_DIR / "config" / "localtunnel.log"
671
671
 
672
672
  def setup_remote_access():
673
- h1("Step 6 of 7 — Remote Access (ngrok / localtunnel)")
673
+ h1("Step 6 of 7 Remote Access (ngrok / localtunnel)")
674
674
 
675
675
  print(f"""
676
676
  The SuperBrain backend runs locally. Your phone needs to reach it over the internet.
@@ -703,11 +703,11 @@ def setup_remote_access():
703
703
  else:
704
704
  warn("No ngrok token provided. ngrok may disconnect. To fix, re-run setup.")
705
705
 
706
- # ══════════════════════════════════════════════════════════════════════════════
707
- # Step 6 — Access Token & Database
708
- # ══════════════════════════════════════════════════════════════════════════════
706
+ # ══════════════════════════════════════════════════════════════════════════════
707
+ # Step 6 Access Token & Database
708
+ # ══════════════════════════════════════════════════════════════════════════════
709
709
  def setup_token_and_db():
710
- h1("Step 7 of 7 — Access Token & Database")
710
+ h1("Step 7 of 7 Access Token & Database")
711
711
 
712
712
  # Token
713
713
  if TOKEN_FILE.exists():
@@ -726,16 +726,31 @@ def setup_token_and_db():
726
726
  TOKEN_FILE.write_text(new_token)
727
727
  ok(f"Access Token saved: {BOLD}{GREEN}{new_token}{RESET}")
728
728
  nl()
729
- print(f" {YELLOW}Copy this token into the mobile app → Settings → Access Token.{RESET}")
729
+ print(f" {YELLOW}Copy this token into the mobile app Settings Access Token.{RESET}")
730
730
 
731
731
  # DB is auto-created on first backend start; just let the user know
732
732
  nl()
733
733
  info("The SQLite database (superbrain.db) will be created automatically")
734
734
  info("the first time the backend starts.")
735
735
 
736
- # ══════════════════════════════════════════════════════════════════════════════
736
+ # ══════════════════════════════════════════════════════════════════════════════
737
737
  # Launch Backend
738
- # ══════════════════════════════════════════════════════════════════════════════
738
+ # ══════════════════════════════════════════════════════════════════════════════
739
+ def _start_ngrok(port: int) -> str | None:
740
+ try:
741
+ import pyngrok
742
+ from pyngrok import ngrok
743
+
744
+ token = NGROK_TOKEN.read_text().strip() if NGROK_TOKEN.exists() else None
745
+ if token:
746
+ ngrok.set_auth_token(token)
747
+
748
+ tunnel = ngrok.connect(port, bind_tls=True)
749
+ return tunnel.public_url
750
+ except Exception as e:
751
+ warn(f"Failed to start ngrok: {e}")
752
+ return None
753
+
739
754
  def _find_localtunnel_url_from_log() -> str | None:
740
755
  try:
741
756
  import re
@@ -755,8 +770,10 @@ def _stop_localtunnel_processes():
755
770
  "| Where-Object { $_.CommandLine -match 'localtunnel|\\.loca\\.lt' } "
756
771
  "| ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"
757
772
  )
773
+ import subprocess
758
774
  subprocess.run(["powershell", "-NoProfile", "-Command", script], check=False)
759
775
  else:
776
+ import subprocess
760
777
  subprocess.run(["pkill", "-f", "localtunnel"], check=False)
761
778
  except Exception:
762
779
  pass
@@ -818,8 +835,10 @@ def _stop_localtunnel_processes():
818
835
  "| Where-Object { $_.CommandLine -match 'localtunnel|\\.loca\\.lt' } "
819
836
  "| ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"
820
837
  )
838
+ import subprocess
821
839
  subprocess.run(["powershell", "-NoProfile", "-Command", script], check=False)
822
840
  else:
841
+ import subprocess
823
842
  subprocess.run(["pkill", "-f", "localtunnel"], check=False)
824
843
  except Exception:
825
844
  pass
@@ -862,21 +881,6 @@ def _start_localtunnel(port: int, timeout: int = 25) -> str | None:
862
881
  return url
863
882
  return None
864
883
 
865
- def _start_ngrok(port: int) -> str | None:
866
- try:
867
- import pyngrok
868
- from pyngrok import ngrok
869
-
870
- token = NGROK_TOKEN.read_text().strip() if NGROK_TOKEN.exists() else None
871
- if token:
872
- ngrok.set_auth_token(token)
873
-
874
- tunnel = ngrok.connect(port, bind_tls=True)
875
- return tunnel.public_url
876
- except Exception as e:
877
- warn(f"Failed to start ngrok: {e}")
878
- return None
879
-
880
884
  def _get_windows_pids_on_port(port: int) -> list[int]:
881
885
  """Return listener PIDs on Windows using Get-NetTCPConnection when available."""
882
886
  pids: set[int] = set()
@@ -907,7 +911,7 @@ def _check_port(port: int) -> int | None:
907
911
  if s.connect_ex(("127.0.0.1", port)) != 0:
908
912
  return None # port is free
909
913
 
910
- # Port is busy — try to find the PID
914
+ # Port is busy try to find the PID
911
915
  try:
912
916
  if IS_WINDOWS:
913
917
  out = run_q(["netstat", "-ano"]).stdout
@@ -1064,7 +1068,7 @@ def _display_connect_qr(url: str, token: str):
1064
1068
  for _ in range(quiet):
1065
1069
  padded.append(list(empty_row))
1066
1070
 
1067
- print(f" ┌{'─' * (padded_cols + 4)}┐")
1071
+ print(f" {'' * (padded_cols + 4)}")
1068
1072
 
1069
1073
  # Center the title within the inner frame
1070
1074
  inner_width = padded_cols + 4
@@ -1073,8 +1077,8 @@ def _display_connect_qr(url: str, token: str):
1073
1077
  if len(title) > inner_width - 2:
1074
1078
  title = title[:inner_width - 2]
1075
1079
 
1076
- print(f" │{title.center(inner_width)}│")
1077
- print(f" ├{'─' * inner_width}┤")
1080
+ print(f" {title.center(inner_width)}")
1081
+ print(f" {'' * inner_width}")
1078
1082
 
1079
1083
  for y in range(0, padded_rows, 2):
1080
1084
  line_chars = []
@@ -1083,17 +1087,17 @@ def _display_connect_qr(url: str, token: str):
1083
1087
  bottom = padded[y + 1][x] if y + 1 < padded_rows else 0
1084
1088
 
1085
1089
  if top and bottom:
1086
- line_chars.append("â–ˆ")
1090
+ line_chars.append("")
1087
1091
  elif top and not bottom:
1088
- line_chars.append("â–€")
1092
+ line_chars.append("")
1089
1093
  elif not top and bottom:
1090
- line_chars.append("â–„")
1094
+ line_chars.append("")
1091
1095
  else:
1092
1096
  line_chars.append(" ")
1093
1097
 
1094
- print(f" │ {''.join(line_chars)} │")
1098
+ print(f" {''.join(line_chars)} ")
1095
1099
 
1096
- print(f" └{'─' * (padded_cols + 4)}┘")
1100
+ print(f" {'' * (padded_cols + 4)}")
1097
1101
  ''')
1098
1102
 
1099
1103
  interpreters = []
@@ -1146,7 +1150,7 @@ def launch_backend():
1146
1150
  # Ensure upload endpoints won't crash FastAPI at import time.
1147
1151
  ensure_runtime_dependencies()
1148
1152
 
1149
- # ── Port conflict check ───────────────────────────────────────────────────
1153
+ # ── Port conflict check ───────────────────────────────────────────────────
1150
1154
  PORT = 5000
1151
1155
  pid = _check_port(PORT)
1152
1156
  if pid is not None:
@@ -1159,7 +1163,7 @@ def launch_backend():
1159
1163
  print(f" This is usually a previous SuperBrain server that wasn't stopped.")
1160
1164
  print(f" Options:")
1161
1165
  print(f" {BOLD}1{RESET} Kill the existing process and start fresh {DIM}(recommended){RESET}")
1162
- print(f" {BOLD}2{RESET} Exit — I'll stop it manually then re-run start.py")
1166
+ print(f" {BOLD}2{RESET} Exit I'll stop it manually then re-run start.py")
1163
1167
  nl()
1164
1168
  choice = input(f" {BOLD}Choose [1/2]{RESET}: ").strip()
1165
1169
 
@@ -1180,7 +1184,7 @@ def launch_backend():
1180
1184
  if IS_WINDOWS:
1181
1185
  killed = _kill_pid_windows(pid)
1182
1186
  if not killed:
1183
- warn(f"PID {pid} is no longer active. Trying current listeners on port {PORT} …")
1187
+ warn(f"PID {pid} is no longer active. Trying current listeners on port {PORT} ")
1184
1188
  else:
1185
1189
  os.kill(pid, _sig.SIGTERM)
1186
1190
  time.sleep(1)
@@ -1208,7 +1212,7 @@ def launch_backend():
1208
1212
  info(f"Run: lsof -ti TCP:{PORT} -sTCP:LISTEN | xargs kill -9")
1209
1213
  sys.exit(1)
1210
1214
  else:
1211
- # Unknown PID — try to kill all listeners we can find
1215
+ # Unknown PID try to kill all listeners we can find
1212
1216
  extra_pids = _find_pids_on_port(PORT)
1213
1217
  if not extra_pids:
1214
1218
  err("Cannot determine PID automatically.")
@@ -1241,7 +1245,7 @@ def launch_backend():
1241
1245
  info(f"Try manually: kill -9 {pid}")
1242
1246
  sys.exit(1)
1243
1247
 
1244
- token = TOKEN_FILE.read_text().strip() if TOKEN_FILE.exists() else "—"
1248
+ token = TOKEN_FILE.read_text().strip() if TOKEN_FILE.exists() else ""
1245
1249
  local_ip = _detect_local_ip()
1246
1250
 
1247
1251
  # tunnel startup
@@ -1258,7 +1262,7 @@ def launch_backend():
1258
1262
  public_url = _start_ngrok(PORT)
1259
1263
  if public_url:
1260
1264
  tunnel_type = "ngrok"
1261
- ok(f"ngrok active → {GREEN}{BOLD}{public_url}{RESET}")
1265
+ ok(f"ngrok active {GREEN}{BOLD}{public_url}{RESET}")
1262
1266
  else:
1263
1267
  warn("ngrok failed. Falling back to localtunnel...")
1264
1268
 
@@ -1266,67 +1270,67 @@ def launch_backend():
1266
1270
  public_url = _start_localtunnel(PORT)
1267
1271
  if public_url:
1268
1272
  tunnel_type = "localtunnel"
1269
- ok(f"localtunnel active → {GREEN}{BOLD}{public_url}{RESET}")
1273
+ ok(f"localtunnel active {GREEN}{BOLD}{public_url}{RESET}")
1270
1274
 
1271
1275
  tunnel_line = ""
1272
1276
  tunnel_hint = ""
1273
1277
 
1274
1278
  if public_url:
1275
- tunnel_line = f" Public URL → {GREEN}{BOLD}{public_url}{RESET} {DIM}({tunnel_type}){RESET}"
1276
- tunnel_hint = f" · public → {GREEN}{public_url}{RESET}"
1279
+ tunnel_line = f" Public URL {GREEN}{BOLD}{public_url}{RESET} {DIM}({tunnel_type}){RESET}"
1280
+ tunnel_hint = f" · public {GREEN}{public_url}{RESET}"
1277
1281
  elif NGROK_ENABLED.exists():
1278
- tunnel_line = f" Public URL → {YELLOW}(failed to start ngrok and localtunnel){RESET}"
1279
- tunnel_hint = f" · public → run manually: {DIM}ngrok http {PORT}{RESET}"
1282
+ tunnel_line = f" Public URL {YELLOW}(failed to start ngrok and localtunnel){RESET}"
1283
+ tunnel_hint = f" · public run manually: {DIM}ngrok http {PORT}{RESET}"
1280
1284
  else:
1281
- tunnel_line = f" Public URL → {YELLOW}(failed to start localtunnel){RESET}"
1282
- tunnel_hint = f" · public → configure ngrok via {DIM}python start.py --reset{RESET} or ensure node/npx is installed"
1285
+ tunnel_line = f" Public URL {YELLOW}(failed to start localtunnel){RESET}"
1286
+ tunnel_hint = f" · public configure ngrok via {DIM}python start.py --reset{RESET} or ensure node/npx is installed"
1283
1287
 
1284
- # ── Generate and display QR code ──────────────────────────────────────────
1288
+ # ── Generate and display QR code ──────────────────────────────────────────
1285
1289
  qr_url = public_url if public_url else f"http://{local_ip}:{PORT}"
1286
1290
  _display_connect_qr(qr_url, token)
1287
1291
 
1288
1292
  print(f"""
1289
1293
  {GREEN}{BOLD}Backend is starting up!{RESET}
1290
1294
 
1291
- Local URL → {CYAN}http://127.0.0.1:{PORT}{RESET}
1292
- Network URL → {CYAN}http://{local_ip}:{PORT}{RESET}
1293
- {(tunnel_line + chr(10)) if tunnel_line else ''} API docs → {CYAN}http://127.0.0.1:{PORT}/docs{RESET}
1294
- Access Token → {BOLD}{MAGENTA}{token}{RESET}
1295
+ Local URL {CYAN}http://127.0.0.1:{PORT}{RESET}
1296
+ Network URL {CYAN}http://{local_ip}:{PORT}{RESET}
1297
+ {(tunnel_line + chr(10)) if tunnel_line else ''} API docs {CYAN}http://127.0.0.1:{PORT}/docs{RESET}
1298
+ Access Token {BOLD}{MAGENTA}{token}{RESET}
1295
1299
 
1296
1300
  {DIM}Keep this terminal open. Press Ctrl+C to stop the server.{RESET}
1297
1301
 
1298
1302
  {YELLOW}Mobile app setup:{RESET}
1299
- {BOLD}Option A — Scan QR code (easiest):{RESET}
1300
- 1. Open the app → Settings → tap the {BOLD}QR icon{RESET} 📷
1303
+ {BOLD}Option A Scan QR code (easiest):{RESET}
1304
+ 1. Open the app Settings tap the {BOLD}QR icon{RESET} 📷
1301
1305
  2. Scan the QR code shown above
1302
- 3. Done — auto-connected!
1306
+ 3. Done auto-connected!
1303
1307
 
1304
- {BOLD}Option B — Manual setup:{RESET}
1308
+ {BOLD}Option B Manual setup:{RESET}
1305
1309
  1. Build / install the SuperBrain APK on your Android device.
1306
- 2. Open the app → tap the ⚙ settings icon.
1310
+ 2. Open the app tap the settings icon.
1307
1311
  3. Set {BOLD}Server URL{RESET} to:
1308
- · Same WiFi → http://{local_ip}:{PORT}
1312
+ · Same WiFi http://{local_ip}:{PORT}
1309
1313
  {tunnel_hint}
1310
- · Port fwd → http://<your-public-ip>:{PORT}
1314
+ · Port fwd http://<your-public-ip>:{PORT}
1311
1315
  4. Set {BOLD}Access Token{RESET} to: {BOLD}{MAGENTA}{token}{RESET}
1312
- 5. Tap {BOLD}Save{RESET} → Connected!
1316
+ 5. Tap {BOLD}Save{RESET} Connected!
1313
1317
 
1314
1318
  {YELLOW}Data Management:{RESET}
1315
- • {BOLD}Export:{RESET} In app Settings → Data Import/Export → choose format (JSON/ZIP)
1316
- • {BOLD}Import:{RESET} Upload backup file in app → Data Import/Export → select file
1317
- • {BOLD}Reset:{RESET} Run {BOLD}python reset.py{RESET} for safe data cleanup options
1319
+ {BOLD}Export:{RESET} In app Settings Data Import/Export choose format (JSON/ZIP)
1320
+ {BOLD}Import:{RESET} Upload backup file in app Data Import/Export select file
1321
+ {BOLD}Reset:{RESET} Run {BOLD}python reset.py{RESET} for safe data cleanup options
1318
1322
 
1319
1323
  {DIM}Security Note: Keep token.txt private. Anyone with this token can use your API.{RESET}
1320
- {DIM}The app securely stores your Access Token locally — it's never transmitted anywhere but your server.{RESET}
1324
+ {DIM}The app securely stores your Access Token locally it's never transmitted anywhere but your server.{RESET}
1321
1325
  """)
1322
1326
 
1323
1327
  os.chdir(BASE_DIR)
1324
1328
  os.execv(str(VENV_PYTHON), [str(VENV_PYTHON), "-m", "uvicorn", "api:app",
1325
1329
  "--host", "0.0.0.0", "--port", str(PORT), "--reload"])
1326
1330
 
1327
- # ══════════════════════════════════════════════════════════════════════════════
1331
+ # ══════════════════════════════════════════════════════════════════════════════
1328
1332
  # Main
1329
- # ══════════════════════════════════════════════════════════════════════════════
1333
+ # ══════════════════════════════════════════════════════════════════════════════
1330
1334
 
1331
1335
  def launch_backend_status():
1332
1336
  h1("SuperBrain Status")
@@ -1337,7 +1341,6 @@ def launch_backend_status():
1337
1341
 
1338
1342
  # Fetch ngrok status via API
1339
1343
  url = "NOT_FOUND"
1340
- tunnel_type = "tunnel"
1341
1344
  try:
1342
1345
  import urllib.request, json
1343
1346
  req = urllib.request.urlopen("http://127.0.0.1:4040/api/tunnels", timeout=2)
@@ -1345,20 +1348,12 @@ def launch_backend_status():
1345
1348
  for tunnel in data.get("tunnels", []):
1346
1349
  if tunnel.get("proto") == "https":
1347
1350
  url = tunnel.get("public_url")
1348
- tunnel_type = "ngrok"
1349
1351
  break
1350
1352
  except Exception:
1351
1353
  pass
1352
1354
 
1353
1355
  if url == "NOT_FOUND":
1354
- # Fallback to localtunnel log
1355
- url_lt = _find_localtunnel_url_from_log()
1356
- if url_lt:
1357
- url = url_lt
1358
- tunnel_type = "localtunnel"
1359
-
1360
- if url == "NOT_FOUND":
1361
- warn("Could not find a running ngrok or localtunnel URL. Is the server running?")
1356
+ warn("Could not find a running ngrok URL. Is the server running?")
1362
1357
  nl()
1363
1358
  print(" Wait 5 seconds, or run 'superbrain-server' to start the server.")
1364
1359
  return
@@ -1370,10 +1365,11 @@ def launch_backend_status():
1370
1365
  network_url = f"http://{local_ip}:5000"
1371
1366
 
1372
1367
  nl()
1373
- print(f" Local URL → {CYAN}{local_url}{RESET}")
1374
- print(f" Network URL → {CYAN}{network_url}{RESET}")
1375
- print(f" Public URL → {CYAN}{url}{RESET} ({tunnel_type})")
1376
- print(f" API docs → {CYAN}{local_url}/docs{RESET}")
1368
+ print(f" Local URL \u2192 {CYAN}{local_url}{RESET}")
1369
+ print(f" Network URL \u2192 {CYAN}{network_url}{RESET}")
1370
+ print(f" Public URL \u2192 {CYAN}{url}{RESET} (localtunnel)")
1371
+ print(f" API docs \u2192 {CYAN}{local_url}/docs{RESET}")
1372
+ print(f" Access Token \u2192 {BOLD}{MAGENTA}{token}{RESET}")
1377
1373
  nl()
1378
1374
 
1379
1375
  def main():
@@ -1396,8 +1392,8 @@ def main():
1396
1392
  reset_mode = "--reset" in sys.argv
1397
1393
 
1398
1394
  if SETUP_DONE.exists() and not reset_mode:
1399
- # Already configured — just launch
1400
- print(f" {GREEN}Setup already complete.{RESET} Starting backend …")
1395
+ # Already configured just launch
1396
+ print(f" {GREEN}Setup already complete.{RESET} Starting backend ")
1401
1397
  print(f" {DIM}Run python start.py --reset to redo the setup wizard.{RESET}")
1402
1398
  launch_backend()
1403
1399
  return
@@ -1405,18 +1401,18 @@ def main():
1405
1401
  print(f"""
1406
1402
  Welcome to SuperBrain! This wizard will guide you through:
1407
1403
 
1408
- 1 · Create Python virtual environment
1409
- 2 · Install all required packages
1410
- 3 · Configure AI provider keys + Instagram credentials
1411
- 4 · Set up an offline AI model via Ollama (qwen3-vl:4b)
1412
- 5 · Set up offline audio transcription (Whisper + ffmpeg)
1413
- 6 · Configure remote access (localtunnel or port forwarding)
1414
- 7 · Generate Access Token & initialise database
1404
+ 1 · Create Python virtual environment
1405
+ 2 · Install all required packages
1406
+ 3 · Configure AI provider keys + Instagram credentials
1407
+ 4 · Set up an offline AI model via Ollama (qwen3-vl:4b)
1408
+ 5 · Set up offline audio transcription (Whisper + ffmpeg)
1409
+ 6 · Configure remote access (localtunnel or port forwarding)
1410
+ 7 · Generate Access Token & initialise database
1415
1411
 
1416
1412
  Press {BOLD}Enter{RESET} to accept defaults shown in [{DIM}brackets{RESET}].
1417
1413
  You can re-run this wizard any time with: {BOLD}python start.py --reset{RESET}
1418
1414
  """)
1419
- input(f" Press {BOLD}Enter{RESET} to begin … ")
1415
+ input(f" Press {BOLD}Enter{RESET} to begin ")
1420
1416
 
1421
1417
  try:
1422
1418
  setup_venv()
@@ -1435,9 +1431,9 @@ def main():
1435
1431
  SETUP_DONE.write_text("ok")
1436
1432
 
1437
1433
  nl()
1438
- print(f" {GREEN}{BOLD}{'═'*60}{RESET}")
1439
- print(f" {GREEN}{BOLD} ✓ Setup complete!{RESET}")
1440
- print(f" {GREEN}{BOLD}{'═'*60}{RESET}")
1434
+ print(f" {GREEN}{BOLD}{''*60}{RESET}")
1435
+ print(f" {GREEN}{BOLD} Setup complete!{RESET}")
1436
+ print(f" {GREEN}{BOLD}{''*60}{RESET}")
1441
1437
  nl()
1442
1438
 
1443
1439
  if ask_yn("Start the backend now?", default=True):
@@ -1447,4 +1443,3 @@ def main():
1447
1443
 
1448
1444
  if __name__ == "__main__":
1449
1445
  main()
1450
-