Files
vocero-client/start_ui.sh
T

165 lines
5.4 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="$ROOT_DIR/.venv"
REQ_FILE="$ROOT_DIR/s2s-client/requirements.txt"
FRONTEND_DIR="$ROOT_DIR/frontend-s2s"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
source "$ROOT_DIR/.env"
set +a
fi
UI_PORT="${UI_PORT:-8420}"
UI_HOST="${UI_HOST:-0.0.0.0}"
S2S_BACKEND_PORT="${S2S_BACKEND_PORT:-8421}"
S2S_PROXY_TARGET="${S2S_PROXY_TARGET:-ws://ai-server:12345/v1/realtime}"
if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then
echo "Error: start_ui.sh requires Bash 4.4 or newer."
exit 1
fi
for required_command in python3 node npm setsid curl; do
if ! command -v "$required_command" >/dev/null 2>&1; then
echo "Error: $required_command was not found."
exit 1
fi
done
if ! node -e 'const [major, minor] = process.versions.node.split(".").map(Number); process.exit((major === 22 && minor >= 18) || major >= 24 ? 0 : 1)'; then
echo "Error: frontend-s2s requires Node.js 22.18+ on the 22.x line, or Node.js 24+."
exit 1
fi
for port_name in UI_PORT S2S_BACKEND_PORT; do
port_value="${!port_name}"
if [[ ! "$port_value" =~ ^[0-9]{1,5}$ ]] || (( 10#$port_value < 1 || 10#$port_value > 65535 )); then
echo "Error: $port_name must be a port between 1 and 65535."
exit 1
fi
# Normalize leading zeroes before passing ports to Node and Uvicorn.
printf -v "$port_name" '%d' "$((10#$port_value))"
done
if [ "$UI_PORT" = "$S2S_BACKEND_PORT" ]; then
echo "Error: UI_PORT and S2S_BACKEND_PORT must be different."
exit 1
fi
if [ ! -d "$VENV_DIR" ]; then
echo "Creating the virtual environment in .venv..."
python3 -m venv "$VENV_DIR"
fi
source "$VENV_DIR/bin/activate"
if [ ! -f "$REQ_FILE" ]; then
echo "Error: requirements file was not found: $REQ_FILE"
exit 1
fi
if ! python -c "import fastapi, uvicorn, httpx, huggingface_hub, dotenv, websockets" >/dev/null 2>&1; then
echo "Installing dependencies from s2s-client/requirements.txt..."
python -m pip install -r "$REQ_FILE"
fi
# Keep restarts offline when the installed frontend manifests have not changed.
if [ ! -f "$FRONTEND_DIR/node_modules/vite/bin/vite.js" ] || \
! cmp -s "$FRONTEND_DIR/package-lock.json" "$FRONTEND_DIR/node_modules/.s2s-package-lock.json" || \
! cmp -s "$FRONTEND_DIR/package.json" "$FRONTEND_DIR/node_modules/.s2s-package.json"; then
echo "Installing frontend dependencies from frontend-s2s/package-lock.json..."
npm --prefix "$FRONTEND_DIR" ci --include=dev
cp "$FRONTEND_DIR/package-lock.json" "$FRONTEND_DIR/node_modules/.s2s-package-lock.json"
cp "$FRONTEND_DIR/package.json" "$FRONTEND_DIR/node_modules/.s2s-package.json"
fi
# The combined launcher always proxies to the backend it starts. An alternate
# S2S_BACKEND_URL is only used when running Vite independently.
export S2S_BACKEND_URL="http://127.0.0.1:$S2S_BACKEND_PORT"
export S2S_PROXY_TARGET
child_pids=()
cleanup() {
local exit_status=$?
trap - EXIT
trap '' INT TERM HUP
for child_pid in "${child_pids[@]}"; do
kill -TERM -- "-$child_pid" 2>/dev/null || true
done
# Stop whole process groups, including Uvicorn's reload worker. Bound the
# graceful shutdown so a stuck connection cannot leave ports occupied.
for ((attempt = 0; attempt < 20; attempt++)); do
local alive=false
for child_pid in "${child_pids[@]}"; do
if kill -0 -- "-$child_pid" 2>/dev/null; then
alive=true
fi
done
if [ "$alive" = false ]; then
break
fi
sleep 0.05
done
for child_pid in "${child_pids[@]}"; do
kill -KILL -- "-$child_pid" 2>/dev/null || true
wait "$child_pid" 2>/dev/null || true
done
exit "$exit_status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP
echo "💻 Starting the React UI at http://$UI_HOST:$UI_PORT (Vite hot reload)..."
echo "🔧 FastAPI backend: $S2S_BACKEND_URL (Python reload enabled)"
echo "🔌 Realtime proxy target: $S2S_PROXY_TARGET"
echo " Press Ctrl+C in this terminal to stop both servers."
echo "----------------------------------------------------"
cd "$ROOT_DIR"
setsid "$VENV_DIR/bin/python" -m uvicorn server:app \
--app-dir "$ROOT_DIR/s2s-client" \
--host 127.0.0.1 --port "$S2S_BACKEND_PORT" \
--reload --reload-dir "$ROOT_DIR/s2s-client" &
backend_pid=$!
child_pids+=("$backend_pid")
backend_ready=false
startup_deadline=$((SECONDS + 30))
while (( SECONDS < startup_deadline )); do
if ! kill -0 "$backend_pid" 2>/dev/null; then
echo "Error: FastAPI exited before it was ready."
exit 1
fi
if curl --noproxy '*' --silent --fail --max-time 1 \
--output /dev/null "$S2S_BACKEND_URL/api/config"; then
backend_ready=true
break
fi
sleep 0.2
done
if [ "$backend_ready" = false ]; then
echo "Error: FastAPI did not become ready within 30 seconds."
exit 1
fi
(
cd "$FRONTEND_DIR"
# Keep React Fast Refresh enabled even if PM2 exports NODE_ENV=production.
export NODE_ENV=development
exec setsid node node_modules/vite/bin/vite.js --host "$UI_HOST" --port "$UI_PORT" --strictPort
) &
frontend_pid=$!
child_pids+=("$frontend_pid")
# PM2 supervises this script. If either server exits, stop its sibling and fail
# the launcher so PM2 can restart the complete pair instead of a partial UI.
exit_status=0
wait -n "$backend_pid" "$frontend_pid" || exit_status=$?
echo "Error: a UI server exited; stopping both servers."
if [ "$exit_status" -eq 0 ]; then
exit_status=1
fi
exit "$exit_status"