cai
2026-08-11 db6463d9f98f740d4e8b6d3a0a25b65688196321
run-local.sh
@@ -1,159 +1,246 @@
#!/usr/bin/env bash
set -euo pipefail
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE_ROOT="${COMBRABO_VOICE_RUNTIME_HELPER_WORKDIR:-${SCRIPT_DIR}}"
WORKSPACE_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
MANIFEST_PATH="${SCRIPT_DIR}/Cargo.toml"
BINARY_PATH="${SCRIPT_DIR}/target/release/combrabo-voice-runtime-helper"
DOCKERFILE_PATH="${SCRIPT_DIR}/Dockerfile"
IMAGE_NAME="${COMBRABO_VOICE_RUNTIME_HELPER_IMAGE:-combrabo-voice-runtime-helper:local}"
MODE="${COMBRABO_VOICE_RUNTIME_HELPER_MODE:-auto}"
usage() {
  cat <<'EOF'
Usage:
  run-local.sh [--prepare] [--rebuild]
Options:
  --prepare   Prepare the current helper runtime mode without starting the helper.
  --rebuild   Force docker image rebuild in docker mode.
EOF
}
PREPARE_ONLY=false
FORCE_REBUILD=false
while [[ $# -gt 0 ]]; do
  case "$1" in
    --prepare)
      PREPARE_ONLY=true
      shift
      ;;
    --rebuild)
      FORCE_REBUILD=true
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      echo "Unknown argument: $1" >&2
      usage >&2
      exit 1
      ;;
  esac
done
resolve_mode() {
  local requested="$1"
  if [[ "${requested}" != "auto" ]]; then
    printf '%s' "${requested}"
    return 0
  fi
  if [[ "$(uname -s)" == "Darwin" ]] && command -v docker >/dev/null 2>&1; then
    printf '%s' "docker"
    return 0
  fi
  printf '%s' "host"
}
prepare_host() {
  (cd "${SCRIPT_DIR}" && cargo build --release)
}
prepare_docker() {
  if ! command -v docker >/dev/null 2>&1; then
    echo "Docker is required for docker helper mode, but docker is not installed." >&2
    exit 1
  fi
  if [[ "${FORCE_REBUILD}" == "true" ]] || ! docker image inspect "${IMAGE_NAME}" >/dev/null 2>&1; then
    docker build -t "${IMAGE_NAME}" "${SCRIPT_DIR}"
  fi
}
rewrite_livekit_url_for_docker() {
  local url="$1"
  if [[ "${url}" =~ ^(ws|wss)://(127\.0\.0\.1|localhost)([:/].*)?$ ]]; then
    printf '%s://host.docker.internal%s' "${BASH_REMATCH[1]}" "${BASH_REMATCH[3]:-}"
    return 0
  fi
  printf '%s' "${url}"
}
run_host() {
  if [[ ! -x "${BINARY_PATH}" ]]; then
    prepare_host
  fi
  exec "${BINARY_PATH}"
}
run_docker() {
  prepare_docker
  local livekit_url="${CV_LIVEKIT_URL:-}"
  local -a docker_args
  docker_args=(
    --rm
    --name "cv-helper-${CV_CALL_ID:-manual}"
DOCKERFILE_PATH="${SCRIPT_DIR}/Dockerfile"
IMAGE_NAME="${COMBRABO_VOICE_RUNTIME_HELPER_IMAGE:-combrabo-voice-runtime-helper:local}"
MODE="${COMBRABO_VOICE_RUNTIME_HELPER_MODE:-auto}"
usage() {
  cat <<'EOF'
Usage:
  run-local.sh [--prepare] [--rebuild]
Options:
  --prepare   Prepare the current helper runtime mode without starting the helper.
  --rebuild   Force docker image rebuild in docker mode.
EOF
}
PREPARE_ONLY=false
FORCE_REBUILD=false
while [[ $# -gt 0 ]]; do
  case "$1" in
    --prepare)
      PREPARE_ONLY=true
      shift
      ;;
    --rebuild)
      FORCE_REBUILD=true
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      echo "Unknown argument: $1" >&2
      usage >&2
      exit 1
      ;;
  esac
done
resolve_mode() {
  local requested="$1"
  if [[ "${requested}" != "auto" ]]; then
    printf '%s' "${requested}"
    return 0
  fi
  if [[ "$(uname -s)" == "Darwin" ]] && command -v docker >/dev/null 2>&1; then
    printf '%s' "docker"
    return 0
  fi
  printf '%s' "host"
}
prepare_host() {
  (cd "${SCRIPT_DIR}" && cargo build --release)
}
prepare_docker() {
  if ! command -v docker >/dev/null 2>&1; then
    echo "Docker is required for docker helper mode, but docker is not installed." >&2
    exit 1
  fi
  if [[ "${FORCE_REBUILD}" == "true" ]] || ! docker image inspect "${IMAGE_NAME}" >/dev/null 2>&1; then
    docker build -t "${IMAGE_NAME}" "${SCRIPT_DIR}"
  fi
}
rewrite_livekit_url_for_docker() {
  local url="$1"
  if [[ "${url}" =~ ^(ws|wss)://(127\.0\.0\.1|localhost)([:/].*)?$ ]]; then
    printf '%s://host.docker.internal%s' "${BASH_REMATCH[1]}" "${BASH_REMATCH[3]:-}"
    return 0
  fi
  printf '%s' "${url}"
}
rewrite_http_url_for_docker() {
  local url="$1"
  if [[ "${url}" =~ ^(http|https)://(127\.0\.0\.1|localhost)([:/].*)?$ ]]; then
    printf '%s://host.docker.internal%s' "${BASH_REMATCH[1]}" "${BASH_REMATCH[3]:-}"
    return 0
  fi
  printf '%s' "${url}"
}
is_truthy() {
  case "${1:-}" in
    true|TRUE|1|yes|YES|on|ON)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}
docker_service_host() {
  local bind_addr="${1:-127.0.0.1:18080}"
  if [[ "${bind_addr}" =~ ^([^:]+):([0-9]+)$ ]]; then
    printf '%s' "${BASH_REMATCH[1]}"
    return 0
  fi
  printf '%s' "127.0.0.1"
}
docker_service_port() {
  local bind_addr="${1:-127.0.0.1:18080}"
  if [[ "${bind_addr}" =~ ^([^:]+):([0-9]+)$ ]]; then
    printf '%s' "${BASH_REMATCH[2]}"
    return 0
  fi
  printf '%s' "18080"
}
run_host() {
  if [[ ! -x "${BINARY_PATH}" ]]; then
    prepare_host
  fi
  exec "${BINARY_PATH}"
}
run_docker() {
  prepare_docker
  local livekit_url="${CV_LIVEKIT_URL:-}"
  local -a docker_args
  docker_args=(
    --rm
    --name "cv-helper-${CV_CALL_ID:-manual}"
    -v "${WORKSPACE_ROOT}:${WORKSPACE_ROOT}"
    -w "${WORKSPACE_ROOT}"
    -e CV_CALL_ID
    -e CV_TRACE_ID
    -e CV_GAME_MATCH_ID
    -e CV_LIVEKIT_URL
    -e CV_LIVEKIT_ROOM_ID
    -e CV_LIVEKIT_BOT_TOKEN
    -e CV_LIVEKIT_BOT_PARTICIPANT_IDENTITY
    -e CV_LIVEKIT_USER_PARTICIPANT_IDENTITY
    -e CV_FIRST_AUDIO_SOURCE
    -w "${WORKSPACE_ROOT}"
    -e CV_CALL_ID
    -e CV_TRACE_ID
    -e CV_GAME_MATCH_ID
    -e CV_LIVEKIT_URL
    -e CV_LIVEKIT_ROOM_ID
    -e CV_LIVEKIT_BOT_TOKEN
    -e CV_LIVEKIT_BOT_PARTICIPANT_IDENTITY
    -e CV_LIVEKIT_USER_PARTICIPANT_IDENTITY
    -e CV_FIRST_AUDIO_SOURCE
    -e CV_GREETING_SOURCE
    -e CV_GREETING_AUDIO_FILE
    -e CV_GREETING_AUDIO_URL
    -e CV_GREETING_TEXT
    -e CV_ROLE_ID
    -e CV_DEVICE_OUTPUT_SMOKE_ENABLED
    -e CV_DEVICE_OUTPUT_DESTINATION_IDENTITIES
    -e CV_AUDIO_DEBUG_DUMP_DIR
    -e CV_ENABLE_USER_AUDIO_OBSERVER
    -e CV_ENABLE_SIMPLE_VAD
    -e CV_VAD_RMS_THRESHOLD
    -e CV_VAD_PEAK_THRESHOLD
    -e CV_VAD_START_FRAMES
    -e CV_VAD_END_SILENCE_MS
    -e CV_VAD_MIN_SPEECH_MS
    -e CV_VAD_MAX_TURN_MS
    -e CV_VAD_INITIAL_IGNORE_MS
    -e CV_VAD_GATE_UNTIL_GREETING_DONE
    -e CV_VAD_POST_GREETING_DELAY_MS
    -e CV_RUNTIME_TURN_BRIDGE_URL
    -e CV_RUNTIME_TURN_BRIDGE_TOKEN
    -e CV_RUNTIME_TURN_ARTIFACT_DIR
    -e CV_RUNTIME_SESSION_NONCE
    -e CV_HELPER_SERVICE_ENABLED
    -e CV_HELPER_SERVICE_BIND
    -e CV_HELPER_AUTH_TOKEN
    -e CV_HELPER_AUTH_TOKEN_LOCAL_DEV
    -e CV_TURN_BRIDGE_TOKEN_LOCAL_DEV
    -e CV_HELPER_AUTH_TOKEN_DEV
    -e CV_TURN_BRIDGE_TOKEN_DEV
  )
  if [[ -n "${livekit_url}" ]]; then
    export CV_LIVEKIT_URL="$(rewrite_livekit_url_for_docker "${livekit_url}")"
  fi
  if [[ -n "${CV_GREETING_AUDIO_FILE:-}" && -f "${CV_GREETING_AUDIO_FILE}" ]]; then
    local audio_parent
    audio_parent="$(cd "$(dirname "${CV_GREETING_AUDIO_FILE}")" && pwd)"
    if [[ "${audio_parent}" != "${WORKSPACE_ROOT}" && "${audio_parent}" != "${WORKSPACE_ROOT}/"* ]]; then
      docker_args+=(-v "${audio_parent}:${audio_parent}:ro")
    fi
  fi
  exec docker run "${docker_args[@]}" "${IMAGE_NAME}"
}
RUNTIME_MODE="$(resolve_mode "${MODE}")"
if [[ "${PREPARE_ONLY}" == "true" ]]; then
  case "${RUNTIME_MODE}" in
    host)
      prepare_host
      ;;
    docker)
      prepare_docker
      ;;
    *)
      echo "Unsupported helper mode: ${RUNTIME_MODE}" >&2
      exit 1
      ;;
  esac
  exit 0
fi
case "${RUNTIME_MODE}" in
  host)
    run_host
    ;;
  docker)
    run_docker
    ;;
  *)
    echo "Unsupported helper mode: ${RUNTIME_MODE}" >&2
    exit 1
    ;;
esac
  if is_truthy "${CV_HELPER_SERVICE_ENABLED:-}"; then
    local service_bind service_host service_port
    service_bind="${CV_HELPER_SERVICE_BIND:-127.0.0.1:18080}"
    service_host="$(docker_service_host "${service_bind}")"
    service_port="$(docker_service_port "${service_bind}")"
    export CV_HELPER_SERVICE_BIND="0.0.0.0:${service_port}"
    docker_args+=(-p "${service_host}:${service_port}:${service_port}")
  fi
  if [[ -n "${livekit_url}" ]]; then
    export CV_LIVEKIT_URL="$(rewrite_livekit_url_for_docker "${livekit_url}")"
  fi
  if [[ -n "${CV_RUNTIME_TURN_BRIDGE_URL:-}" ]]; then
    export CV_RUNTIME_TURN_BRIDGE_URL="$(rewrite_http_url_for_docker "${CV_RUNTIME_TURN_BRIDGE_URL}")"
  fi
  if [[ -n "${CV_GREETING_AUDIO_FILE:-}" && -f "${CV_GREETING_AUDIO_FILE}" ]]; then
    local audio_parent
    audio_parent="$(cd "$(dirname "${CV_GREETING_AUDIO_FILE}")" && pwd)"
    if [[ "${audio_parent}" != "${WORKSPACE_ROOT}" && "${audio_parent}" != "${WORKSPACE_ROOT}/"* ]]; then
      docker_args+=(-v "${audio_parent}:${audio_parent}:ro")
    fi
  fi
  if [[ -n "${CV_AUDIO_DEBUG_DUMP_DIR:-}" ]]; then
    mkdir -p "${CV_AUDIO_DEBUG_DUMP_DIR}"
    local debug_dump_dir
    debug_dump_dir="$(cd "${CV_AUDIO_DEBUG_DUMP_DIR}" && pwd)"
    export CV_AUDIO_DEBUG_DUMP_DIR="${debug_dump_dir}"
    if [[ "${debug_dump_dir}" != "${WORKSPACE_ROOT}" && "${debug_dump_dir}" != "${WORKSPACE_ROOT}/"* ]]; then
      docker_args+=(-v "${debug_dump_dir}:${debug_dump_dir}")
    fi
  fi
  if [[ -n "${CV_RUNTIME_TURN_ARTIFACT_DIR:-}" ]]; then
    mkdir -p "${CV_RUNTIME_TURN_ARTIFACT_DIR}"
    local turn_artifact_dir
    turn_artifact_dir="$(cd "${CV_RUNTIME_TURN_ARTIFACT_DIR}" && pwd)"
    export CV_RUNTIME_TURN_ARTIFACT_DIR="${turn_artifact_dir}"
    if [[ "${turn_artifact_dir}" != "${WORKSPACE_ROOT}" && "${turn_artifact_dir}" != "${WORKSPACE_ROOT}/"* ]]; then
      docker_args+=(-v "${turn_artifact_dir}:${turn_artifact_dir}")
    fi
  fi
  exec docker run "${docker_args[@]}" "${IMAGE_NAME}"
}
RUNTIME_MODE="$(resolve_mode "${MODE}")"
if [[ "${PREPARE_ONLY}" == "true" ]]; then
  case "${RUNTIME_MODE}" in
    host)
      prepare_host
      ;;
    docker)
      prepare_docker
      ;;
    *)
      echo "Unsupported helper mode: ${RUNTIME_MODE}" >&2
      exit 1
      ;;
  esac
  exit 0
fi
case "${RUNTIME_MODE}" in
  host)
    run_host
    ;;
  docker)
    run_docker
    ;;
  *)
    echo "Unsupported helper mode: ${RUNTIME_MODE}" >&2
    exit 1
    ;;
esac