#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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}" } 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 -e CV_GREETING_SOURCE -e CV_GREETING_AUDIO_FILE -e CV_GREETING_AUDIO_URL -e CV_GREETING_TEXT -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 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