Sanad_lite/shell_scripts/start_cpanel.sh
Kassam Dakhlalah 9b7ef7d939 Device selection, cross-browser Live audio, and a rewritten README
Live Gemini could not use an external speaker or headset. An AudioContext
is bound to whichever output was default when it was created, and
getUserMedia({audio:true}) takes the system default input, so plugging a
device in afterwards left audio going to the old one — silently, with no
error to explain it.

  * Explicit Mic and Speaker pickers. Capture opens the chosen deviceId
    exactly and can be switched mid-session; playback is routed through a
    MediaStreamAudioDestinationNode into a hidden <audio> element so
    setSinkId() can move it to the chosen sink. Both lists refresh on
    devicechange and are remembered in localStorage.
  * Windows reports each device three times (default, communications, and
    the real one), so an Anker would have appeared three times with no way
    to tell them apart. The pseudo-devices are now collapsed.
  * Capture moved to an AudioWorklet (Blob-built, no extra file served)
    with the ScriptProcessor kept as a fallback.
  * A compatibility line reports what the browser actually supports, and
    the diagnostics line now shows the output sink and capture kind — the
    difference between "not listening" and "not speaking" without a
    debugger.

Verified in both engines with Playwright (chromium PASS, firefox PASS: no
page errors, personas and device lists populated, test tone plays) and
against real hardware, where setSinkId matched the selection and a named
microphone opened by deviceId. Two engine-specific bugs fell out of that
run and are fixed here: reading AudioContext.prototype.audioWorklet invokes
the getter and throws in both browsers, which aborted init and left the tab
empty, and the init steps are now isolated so one failure cannot take the
rest down.

Also in this commit:

  * README rewritten against what the code does today — the voice-fidelity
    rationale and its three gates, why words used to cut off, the Live tab
    and why the browser talks to Google directly, personas, device
    selection, recordings search/filter, sign-in history, a current API
    list, the cPanel production setup, and the known limits.
  * shell_scripts/start_cpanel.sh — the launcher that actually keeps the
    site up (HTTP health check, not a TCP probe) was only on the host.
  * data/live_personas.json — the persona library (G1, R1, Agibot, T800)
    existed only on the server; it is user-written content worth keeping.
  * .gitignore covers runtime state (logins, generated WAVs, .env, backups).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 23:33:28 +04:00

66 lines
3.1 KiB
Bash

#!/bin/bash
# Sanad_lite uvicorn launcher. Idempotent — exits cleanly if uvicorn is
# already SERVING on :8000. Designed to be called every 2 min by cron.
#
# This is the copy that actually runs in production, kept here so the repo
# is not missing the piece that keeps the site up. It lives on the host at
# /home2/sanadliteysloota/sanadlite/start.sh
# and is invoked by:
# */2 * * * * /home2/sanadliteysloota/sanadlite/start.sh
# The absolute paths below are that account's; change them for another host.
# --- 1. Health check: is the app actually SERVING HTTP on :8000? ----------
# A plain TCP-connect probe is not enough on its own: a wedged uvicorn (frozen
# asyncio loop) still holds the port and completes the TCP handshake, so a
# connect-only check reports "up" and never restarts a hung app (this bit us —
# the site was down ~2 days on a bound-but-frozen process). So: first cheaply
# check whether anything is bound (bash /dev/tcp), and if so require a real
# HTTP response too. curl prints "000" on timeout (hung); any real status code
# (200/303/401/...) means the event loop is alive and serving.
if (exec 3<>/dev/tcp/127.0.0.1/8000) 2>/dev/null; then
exec 3<&-; exec 3>&-
CODE="$(curl -s -o /dev/null -m 8 -w '%{http_code}' http://127.0.0.1:8000/api/health 2>/dev/null)"
if [ -n "$CODE" ] && [ "$CODE" != "000" ]; then
exit 0 # bound AND serving → healthy, nothing to do
fi
# Bound but not responding → wedged. Kill the frozen instance so we can
# relaunch cleanly. Pattern is anchored to the venv path so it only ever
# matches our own uvicorn, never this script or the selector helper.
echo "[start.sh] $(date) — :8000 bound but not serving (code=${CODE:-none}); killing frozen instance" >> /home2/sanadliteysloota/sanadlite/logs/uvicorn.log
pkill -9 -f '/home2/sanadliteysloota/virtualenv/sanadlite/.*main.py' 2>/dev/null
sleep 2
fi
cd /home2/sanadliteysloota/sanadlite
# --- 2. Activate the venv (puts python3 on PATH, sets VIRTUAL_ENV) --------
source /home2/sanadliteysloota/virtualenv/sanadlite/3.8/bin/activate
# --- 3. Source env vars set in cPanel "Setup Python App" form -------------
# CloudLinux's Python Selector stores them in ~/.cl.selector/python-selector.json
# but the activate script does NOT re-export them. Parse + export here.
SELECTOR_JSON="$HOME/.cl.selector/python-selector.json"
APP_NAME="sanadlite"
if [ -f "$SELECTOR_JSON" ]; then
eval "$(python - <<PYEOF
import json, shlex
try:
data = json.load(open("$SELECTOR_JSON"))
except Exception:
pass
else:
app = data.get("$APP_NAME") or {}
for k, v in (app.get("env_vars") or {}).items():
print(f"export {shlex.quote(str(k))}={shlex.quote(str(v))}")
PYEOF
)"
fi
# --- 4. Standard runtime env ---------------------------------------------
export SANAD_DASHBOARD_HOST=127.0.0.1
# --- 5. Launch uvicorn in the background ---------------------------------
echo "[start.sh] $(date) — launching uvicorn on :8000" >> /home2/sanadliteysloota/sanadlite/logs/uvicorn.log
nohup python main.py >> /home2/sanadliteysloota/sanadlite/logs/uvicorn.log 2>&1 &
disown