#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" if [[ ! -d ".venv" ]]; then python3 -m venv .venv fi source .venv/bin/activate pip install -r requirements.txt >/dev/null FRONTEND_DIR="${SCRIPT_DIR}/frontend" : "${FRONTEND_BUILD_ON_START:=1}" resolve_frontend_runner() { if command -v bun >/dev/null 2>&1; then echo "bun" return 0 fi if [[ -x "${HOME}/.bun/bin/bun" ]]; then echo "${HOME}/.bun/bin/bun" return 0 fi if command -v npm >/dev/null 2>&1; then echo "npm" return 0 fi return 1 } build_frontend_bundle() { if [[ ! -f "${FRONTEND_DIR}/package.json" ]]; then echo "Frontend package.json not found at ${FRONTEND_DIR}" >&2 return 1 fi local runner if ! runner="$(resolve_frontend_runner)"; then echo "Unable to build frontend: install Bun or npm first." >&2 return 1 fi if [[ ! -d "${FRONTEND_DIR}/node_modules" ]]; then echo "Frontend dependencies are missing at ${FRONTEND_DIR}/node_modules." >&2 if [[ "${runner}" == "bun" || "${runner}" == "${HOME}/.bun/bin/bun" ]]; then ( cd "${FRONTEND_DIR}" "${runner}" install --frozen-lockfile ) else echo "Install frontend dependencies before starting the server." >&2 return 1 fi fi echo "Building frontend bundle from ${FRONTEND_DIR}" if [[ "${runner}" == "npm" ]]; then ( cd "${FRONTEND_DIR}" npm run build ) else ( cd "${FRONTEND_DIR}" "${runner}" run build ) fi } if [[ "${FRONTEND_BUILD_ON_START}" == "1" ]]; then build_frontend_bundle fi # Optional local voice settings. Example file: .env.voice if [[ -f ".env.voice" ]]; then set -a # shellcheck disable=SC1091 source ".env.voice" set +a fi # Nanobot command defaults (prefer Nanobot's own virtualenv interpreter). : "${NANOBOT_WORKDIR:=${HOME}/nanobot}" if [[ -z "${NANOBOT_COMMAND:-}" ]]; then if [[ -x "${NANOBOT_WORKDIR}/.venv/bin/python" ]]; then NANOBOT_COMMAND="${NANOBOT_WORKDIR}/.venv/bin/python -m nanobot agent --no-markdown" elif [[ -x "${NANOBOT_WORKDIR}/venv/bin/python" ]]; then NANOBOT_COMMAND="${NANOBOT_WORKDIR}/venv/bin/python -m nanobot agent --no-markdown" fi fi export NANOBOT_WORKDIR NANOBOT_COMMAND : "${NANOBOT_SUPPRESS_NOISY_UI:=1}" : "${NANOBOT_OUTPUT_DEDUP_WINDOW_S:=1.5}" export NANOBOT_SUPPRESS_NOISY_UI NANOBOT_OUTPUT_DEDUP_WINDOW_S # Host voice pipeline env vars (safe defaults). : "${HOST_STT_PROVIDER:=faster-whisper}" : "${HOST_STT_COMMAND:=}" : "${HOST_STT_MODEL:=distil-large-v3}" : "${HOST_STT_DEVICE:=auto}" : "${HOST_STT_COMPUTE_TYPE:=int8}" : "${HOST_STT_LANGUAGE:=en}" : "${HOST_STT_BEAM_SIZE:=2}" : "${HOST_STT_BEST_OF:=2}" : "${HOST_STT_VAD_FILTER:=0}" : "${HOST_STT_TEMPERATURE:=0.0}" : "${HOST_STT_LOG_PROB_THRESHOLD:=-1.0}" : "${HOST_STT_NO_SPEECH_THRESHOLD:=0.6}" : "${HOST_STT_COMPRESSION_RATIO_THRESHOLD:=2.4}" : "${HOST_STT_INITIAL_PROMPT:=Transcribe brief spoken English precisely. Prefer common words over sound effects.}" : "${HOST_STT_REPETITION_PENALTY:=1.0}" : "${HOST_STT_HALLUCINATION_SILENCE_THRESHOLD:=}" : "${HOST_TTS_PROVIDER:=supertonic}" : "${HOST_TTS_COMMAND:=}" : "${SUPERTONIC_MODEL:=supertonic-2}" : "${SUPERTONIC_VOICE_STYLE:=F1}" : "${SUPERTONIC_LANG:=en}" : "${SUPERTONIC_TOTAL_STEPS:=8}" : "${SUPERTONIC_SPEED:=1.5}" : "${SUPERTONIC_INTRA_OP_THREADS:=1}" : "${SUPERTONIC_INTER_OP_THREADS:=1}" : "${SUPERTONIC_AUTO_DOWNLOAD:=1}" : "${HOST_STT_MIN_PTT_MS:=220}" : "${HOST_STT_SEGMENT_QUEUE_SIZE:=2}" : "${HOST_STT_BACKLOG_NOTICE_INTERVAL_S:=6.0}" : "${HOST_STT_SUPPRESS_DURING_TTS:=1}" : "${HOST_STT_SUPPRESS_MS_AFTER_TTS:=300}" : "${HOST_RTC_OUTBOUND_LEAD_IN_MS:=120}" : "${HOST_RTC_OUTBOUND_IDLE_S:=0.6}" export HOST_STT_PROVIDER HOST_STT_COMMAND HOST_STT_MODEL HOST_STT_DEVICE export HOST_STT_COMPUTE_TYPE HOST_STT_LANGUAGE HOST_STT_BEAM_SIZE HOST_STT_BEST_OF HOST_STT_VAD_FILTER export HOST_STT_TEMPERATURE HOST_STT_LOG_PROB_THRESHOLD HOST_STT_NO_SPEECH_THRESHOLD export HOST_STT_COMPRESSION_RATIO_THRESHOLD export HOST_STT_INITIAL_PROMPT HOST_STT_REPETITION_PENALTY HOST_STT_HALLUCINATION_SILENCE_THRESHOLD export HOST_TTS_PROVIDER HOST_TTS_COMMAND export SUPERTONIC_MODEL SUPERTONIC_VOICE_STYLE SUPERTONIC_LANG export SUPERTONIC_TOTAL_STEPS SUPERTONIC_SPEED export SUPERTONIC_INTRA_OP_THREADS SUPERTONIC_INTER_OP_THREADS SUPERTONIC_AUTO_DOWNLOAD export HOST_STT_MIN_PTT_MS HOST_STT_SEGMENT_QUEUE_SIZE export HOST_STT_BACKLOG_NOTICE_INTERVAL_S export HOST_STT_SUPPRESS_DURING_TTS HOST_STT_SUPPRESS_MS_AFTER_TTS export HOST_RTC_OUTBOUND_LEAD_IN_MS HOST_RTC_OUTBOUND_IDLE_S : "${UVICORN_HOST:=0.0.0.0}" : "${UVICORN_PORT:=8000}" : "${ENABLE_HTTPS:=1}" : "${SSL_DAYS:=365}" : "${SSL_CERT_FILE:=.certs/local-cert.pem}" : "${SSL_KEY_FILE:=.certs/local-key.pem}" if [[ "$ENABLE_HTTPS" == "1" ]]; then mkdir -p "$(dirname "$SSL_CERT_FILE")" mkdir -p "$(dirname "$SSL_KEY_FILE")" if [[ ! -f "$SSL_CERT_FILE" || ! -f "$SSL_KEY_FILE" ]]; then LOCAL_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" SAN_ENTRIES="DNS:localhost,IP:127.0.0.1" if [[ -n "${LOCAL_IP:-}" ]]; then SAN_ENTRIES="${SAN_ENTRIES},IP:${LOCAL_IP}" fi echo "Generating local TLS certificate at '$SSL_CERT_FILE' (SAN: ${SAN_ENTRIES})" openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days "$SSL_DAYS" \ -keyout "$SSL_KEY_FILE" \ -out "$SSL_CERT_FILE" \ -subj "/CN=localhost" \ -addext "subjectAltName=${SAN_ENTRIES}" \ -addext "keyUsage=digitalSignature,keyEncipherment" \ -addext "extendedKeyUsage=serverAuth" fi echo "Starting HTTPS server on https://localhost:${UVICORN_PORT}" exec uvicorn app:app --host "$UVICORN_HOST" --port "$UVICORN_PORT" \ --ssl-certfile "$SSL_CERT_FILE" \ --ssl-keyfile "$SSL_KEY_FILE" fi echo "Starting HTTP server on http://localhost:${UVICORN_PORT}" exec uvicorn app:app --host "$UVICORN_HOST" --port "$UVICORN_PORT"