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>
66 lines
3.1 KiB
Bash
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
|