| #!/usr/bin/env bash |
| |
| |
| set -euo pipefail |
|
|
| ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| PYTHON_BIN="${PYTHON_BIN:-python3}" |
| INSTALL_DEPS="${AXMEETING_INSTALL_DEPS:-AUTO}" |
| INSTALL_WHEEL="${AXMEETING_INSTALL_WHEEL:-AUTO}" |
| BUILD_IF_MISSING="${AXMEETING_BUILD_IF_MISSING:-OFF}" |
| VERIFY_TLS="${AXMEETING_VERIFY_TLS:-ON}" |
| BIND_HOST="${HOST:-0.0.0.0}" |
| PORT="${PORT:-8000}" |
| PUBLIC_HOST="${AXMEETING_PUBLIC_HOST:-}" |
| ASR_BACKEND="${ASR_BACKEND:-sensevoice}" |
| ACCELERATOR="${AXMEETING_ACCELERATOR:-auto}" |
| MODEL_DIR="${AX_MODEL_DIR:-${ROOT_DIR}/models}" |
| |
| |
| BSP_MSP_DIR="${AXMEETING_BSP_MSP_DIR:-/soc}" |
| AXCL_DIR="${AXMEETING_AXCL_DIR:-}" |
| if [[ -n "${AXCL_DIR}" ]]; then |
| AXCL_LIBRARY_DIR="${AXCL_DIR}/lib" |
| else |
| AXCL_LIBRARY_DIR="/usr/lib/axcl" |
| fi |
| CERT_DIR="${ROOT_DIR}/certs" |
| STATE_ROOT="${AXMEETING_STATE_DIR:-${ROOT_DIR}/.runtime}" |
| RUNTIME_REQUIREMENTS="${ROOT_DIR}/requirements-runtime.txt" |
| export SSL_CERT="${SSL_CERT:-${CERT_DIR}/cert.pem}" |
| export SSL_KEY="${SSL_KEY:-${CERT_DIR}/key.pem}" |
| |
| |
| export AXMEETING_UVICORN_LOG_LEVEL="${AXMEETING_UVICORN_LOG_LEVEL:-warning}" |
| export AXMEETING_ACCESS_LOG="${AXMEETING_ACCESS_LOG:-OFF}" |
| export AXMEETING_AX_LOG_LEVEL="${AXMEETING_AX_LOG_LEVEL:-WARN}" |
| export PYTHONUNBUFFERED=1 |
| export PIP_DISABLE_PIP_VERSION_CHECK=1 |
|
|
| usage() { |
| cat <<'EOF' |
| Usage: ./run_web_studio.sh [--port PORT] [--check] |
|
|
| Required deployment assets: |
| - external models/ root (both SenseVoice and FireRed/Punc because the Web UI |
| exposes both backends) |
| - AX650 BSP msp/out, or the AXCL runtime library |
| - a browser-trusted HTTPS certificate and matching private key |
|
|
| Useful environment variables: |
| AX_MODEL_DIR=/opt/ax-meeting/models |
| AXMEETING_BSP_MSP_DIR=/soc |
| AXMEETING_ACCELERATOR=auto|ax650|axcl |
| AXMEETING_AXCL_DIR=/opt/axcl |
| HOST=0.0.0.0 PORT=8000 AXMEETING_PUBLIC_HOST=10.0.0.20 |
| SSL_CERT=/path/cert.pem SSL_KEY=/path/key.pem |
| AXMEETING_WHEEL=/path/to/compatible.whl |
| AXMEETING_BUILD_IF_MISSING=ON |
| AXMEETING_INSTALL_DEPS=ON|OFF|AUTO |
| AXMEETING_INSTALL_WHEEL=ON|OFF|AUTO |
| AXMEETING_VERIFY_TLS=ON|OFF |
| --port, -p PORT |
| --check |
| EOF |
| } |
|
|
| normalize_toggle() { |
| local name="$1" |
| local value="${2^^}" |
| case "${value}" in |
| ON|OFF|AUTO) printf '%s\n' "${value}" ;; |
| *) |
| echo "ERROR: ${name} must be AUTO, ON, or OFF; got: ${2}" >&2 |
| exit 2 |
| ;; |
| esac |
| } |
|
|
| ax650_runtime_available() { |
| [[ -d "${BSP_MSP_DIR}/include" && -d "${BSP_MSP_DIR}/lib" ]] || return 1 |
| local library |
| for library in libax_sys.so libax_engine.so libax_interpreter.so libax_dmadim.so; do |
| [[ -r "${BSP_MSP_DIR}/lib/${library}" ]] || return 1 |
| done |
| } |
|
|
| axcl_runtime_available() { |
| [[ -r "${AXCL_LIBRARY_DIR}/libaxcl_rt.so" || -r "${AXCL_LIBRARY_DIR}/libaxcl_rt.so.1" ]] |
| } |
|
|
| select_accelerator() { |
| case "${ACCELERATOR}" in |
| ax650|axcl) ;; |
| auto) |
| if axcl_runtime_available && ! ax650_runtime_available; then |
| ACCELERATOR=axcl |
| elif ax650_runtime_available && ! axcl_runtime_available; then |
| ACCELERATOR=ax650 |
| elif axcl_runtime_available && ax650_runtime_available; then |
| echo "ERROR: both AX650 and AXCL runtimes are available; set AXMEETING_ACCELERATOR=ax650 or axcl." >&2 |
| exit 2 |
| else |
| echo "ERROR: no supported AX runtime was found." >&2 |
| echo "Expected AX650 BSP at ${BSP_MSP_DIR} or AXCL runtime at ${AXCL_LIBRARY_DIR}." >&2 |
| exit 1 |
| fi |
| ;; |
| *) |
| echo "ERROR: AXMEETING_ACCELERATOR must be auto, ax650, or axcl; got: ${ACCELERATOR}" >&2 |
| exit 2 |
| ;; |
| esac |
| if [[ "${ACCELERATOR}" == axcl ]] && ! axcl_runtime_available; then |
| echo "ERROR: AXCL runtime libaxcl_rt.so is missing from ${AXCL_LIBRARY_DIR}." >&2 |
| echo "Install the AXCL runtime package or set AXMEETING_AXCL_DIR." >&2 |
| exit 1 |
| fi |
| if [[ "${ACCELERATOR}" == ax650 ]] && ! ax650_runtime_available; then |
| echo "ERROR: AX650 BSP msp/out is incomplete at ${BSP_MSP_DIR}." >&2 |
| echo "Set AXMEETING_BSP_MSP_DIR to the matching BSP path." >&2 |
| exit 1 |
| fi |
| } |
|
|
| runtime_dependencies_available() { |
| "${PYTHON_BIN}" - <<'PY' |
| import importlib.util |
| import sys |
|
|
| modules = ( |
| "numpy", "sentencepiece", "scipy", "sklearn", "fastcluster", |
| "fastapi", "uvicorn", "websockets", "openai", "soundfile", "multipart", |
| ) |
| missing = [module for module in modules if importlib.util.find_spec(module) is None] |
| if missing: |
| print("Missing Python runtime modules: " + ", ".join(missing), file=sys.stderr) |
| raise SystemExit(1) |
| PY |
| } |
|
|
| installed_web_package_available() { |
| "${PYTHON_BIN}" -c 'import ax_meeting_studio.web.server' >/dev/null 2>&1 |
| } |
|
|
| wheel_digest() { |
| "${PYTHON_BIN}" - "$1" <<'PY' |
| import hashlib |
| import sys |
|
|
| digest = hashlib.sha256() |
| with open(sys.argv[1], "rb") as source: |
| for block in iter(lambda: source.read(1024 * 1024), b""): |
| digest.update(block) |
| print(digest.hexdigest()) |
| PY |
| } |
|
|
| |
| |
| |
| |
| |
| select_compatible_wheel() { |
| "${PYTHON_BIN}" - "$@" <<'WHEELPY' |
| import os |
| import platform |
| import sys |
|
|
| paths = sys.argv[1:] |
| if not paths: |
| raise SystemExit(1) |
| backend = os.environ.get("AXMEETING_ACCELERATOR", "auto").lower() |
|
|
| def backend_compatible(path, version_text=None): |
| text = (version_text or os.path.basename(path)).lower() |
| if backend == "axcl": |
| return "+axcl" in text |
| if backend == "ax650": |
| return "+axcl" not in text |
| return True |
|
|
| def fallback_tags(): |
| major, minor = sys.version_info[:2] |
| implementation = platform.python_implementation() |
| if implementation == "CPython": |
| python_tag = f"cp{major}{minor}" |
| abi_tags = (python_tag, "abi3", "none") |
| else: |
| python_tag = f"py{major}" |
| abi_tags = ("none",) |
| machine = platform.machine().lower() |
| platform_tags = { |
| "aarch64": ("linux_aarch64", "manylinux2014_aarch64"), |
| "arm64": ("linux_aarch64", "manylinux2014_aarch64"), |
| "x86_64": ("linux_x86_64", "manylinux2014_x86_64"), |
| "amd64": ("linux_x86_64", "manylinux2014_x86_64"), |
| "armv7l": ("linux_armv7l", "manylinux2014_armv7l"), |
| }.get(machine, (f"linux_{machine}",)) |
| return { |
| f"{py}-{abi}-{plat}" |
| for py in (python_tag, "py3", "py2", "py") |
| for abi in abi_tags |
| for plat in platform_tags |
| } | {"py3-none-any", "py-none-any"} |
|
|
| try: |
| from packaging.tags import sys_tags |
| from packaging.utils import parse_wheel_filename |
| except ImportError: |
| supported = fallback_tags() |
| parsed = [] |
| for path in paths: |
| if not backend_compatible(path): |
| continue |
| fields = os.path.basename(path)[:-4].split("-") |
| if len(fields) < 5: |
| continue |
| for py in fields[-3].split("."): |
| for abi in fields[-2].split("."): |
| for plat in fields[-1].split("."): |
| if f"{py}-{abi}-{plat}" in supported: |
| parsed.append((0, os.path.basename(path), path)) |
| break |
| else: |
| continue |
| break |
| else: |
| continue |
| break |
| if parsed: |
| print(max(parsed)[2]) |
| raise SystemExit(0) |
| raise SystemExit(1) |
|
|
| supported = {tag: index for index, tag in enumerate(sys_tags())} |
| compatible = [] |
| for path in paths: |
| try: |
| _, version, build, tags = parse_wheel_filename(os.path.basename(path)) |
| except Exception: |
| continue |
| if not backend_compatible(path, str(version)): |
| continue |
| ranks = [supported[tag] for tag in tags if tag in supported] |
| if ranks: |
| compatible.append((min(ranks), version, build, os.path.basename(path), path)) |
|
|
| if not compatible: |
| raise SystemExit(1) |
|
|
| best_rank = min(item[0] for item in compatible) |
| best = max((item for item in compatible if item[0] == best_rank), |
| key=lambda item: (item[1], item[2], item[3])) |
| print(best[4]) |
| WHEELPY |
| } |
|
|
| display_host() { |
| if [[ -n "${PUBLIC_HOST}" ]]; then |
| printf '%s\n' "${PUBLIC_HOST}" |
| elif [[ "${BIND_HOST}" == "0.0.0.0" || "${BIND_HOST}" == "::" ]]; then |
| "${PYTHON_BIN}" - <<'PY' |
| import socket |
| try: |
| with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as connection: |
| connection.connect(("8.8.8.8", 80)) |
| print(connection.getsockname()[0]) |
| except OSError: |
| print("127.0.0.1") |
| PY |
| else |
| printf '%s\n' "${BIND_HOST}" |
| fi |
| } |
|
|
| ensure_tls() { |
| if [[ "${VERIFY_TLS}" == "OFF" ]]; then |
| return |
| fi |
| if ! command -v openssl >/dev/null 2>&1; then |
| echo "ERROR: openssl is required for TLS validation; set AXMEETING_VERIFY_TLS=OFF only for diagnostics." >&2 |
| exit 1 |
| fi |
| if ! openssl x509 -in "${SSL_CERT}" -noout >/dev/null 2>&1; then |
| echo "ERROR: SSL_CERT is not a readable X.509 certificate: ${SSL_CERT}" >&2 |
| exit 1 |
| fi |
| if ! openssl pkey -in "${SSL_KEY}" -noout >/dev/null 2>&1; then |
| echo "ERROR: SSL_KEY is not a readable private key: ${SSL_KEY}" >&2 |
| exit 1 |
| fi |
| if ! openssl x509 -in "${SSL_CERT}" -noout -checkend 0 >/dev/null 2>&1; then |
| echo "ERROR: HTTPS certificate is expired: ${SSL_CERT}" >&2 |
| exit 1 |
| fi |
| if ! cmp -s \ |
| <(openssl x509 -in "${SSL_CERT}" -pubkey -noout | openssl pkey -pubin -pubout) \ |
| <(openssl pkey -in "${SSL_KEY}" -pubout); then |
| echo "ERROR: SSL_CERT and SSL_KEY do not form a matching key pair." >&2 |
| exit 1 |
| fi |
| } |
|
|
| ensure_runtime_dependencies() { |
| if runtime_dependencies_available; then |
| return |
| fi |
| if [[ "${INSTALL_DEPS}" == "OFF" || "${CHECK_ONLY}" == true ]]; then |
| echo "ERROR: Python runtime dependencies are missing; preflight will not install them." >&2 |
| echo "Install ${RUNTIME_REQUIREMENTS} with ${PYTHON_BIN}, then retry." >&2 |
| exit 1 |
| fi |
| echo "Installing missing Python runtime dependencies..." |
| "${PYTHON_BIN}" -m pip install --user -r "${RUNTIME_REQUIREMENTS}" |
| runtime_dependencies_available |
| } |
|
|
| CHECK_ONLY=false |
| while (( $# > 0 )); do |
| case "$1" in |
| --help|-h) |
| usage |
| exit 0 |
| ;; |
| --check) |
| CHECK_ONLY=true |
| shift |
| ;; |
| --port|-p) |
| if (( $# < 2 )); then |
| echo "ERROR: $1 requires a port number." >&2 |
| usage >&2 |
| exit 2 |
| fi |
| PORT="$2" |
| shift 2 |
| ;; |
| --port=*) |
| PORT="${1#--port=}" |
| shift |
| ;; |
| *) |
| echo "ERROR: unsupported argument: $1" >&2 |
| usage >&2 |
| exit 2 |
| ;; |
| esac |
| done |
|
|
| INSTALL_DEPS="$(normalize_toggle AXMEETING_INSTALL_DEPS "${INSTALL_DEPS}")" |
| INSTALL_WHEEL="$(normalize_toggle AXMEETING_INSTALL_WHEEL "${INSTALL_WHEEL}")" |
| BUILD_IF_MISSING="$(normalize_toggle AXMEETING_BUILD_IF_MISSING "${BUILD_IF_MISSING}")" |
| VERIFY_TLS="$(normalize_toggle AXMEETING_VERIFY_TLS "${VERIFY_TLS}")" |
| [[ "${BUILD_IF_MISSING}" == "AUTO" ]] && BUILD_IF_MISSING="OFF" |
| [[ "${VERIFY_TLS}" == "AUTO" ]] && VERIFY_TLS="ON" |
| case "${ASR_BACKEND}" in |
| sensevoice|firered_punc) ;; |
| *) |
| echo "ERROR: ASR_BACKEND must be sensevoice or firered_punc, got: ${ASR_BACKEND}" >&2 |
| exit 2 |
| ;; |
| esac |
| if [[ ! "${PORT}" =~ ^[0-9]+$ ]] || (( PORT < 1 || PORT > 65535 )); then |
| echo "ERROR: PORT must be an integer between 1 and 65535, got: ${PORT}" >&2 |
| exit 2 |
| fi |
| select_accelerator |
| export AXMEETING_ACCELERATOR="${ACCELERATOR}" |
| if [[ ! -r "${SSL_CERT}" || ! -r "${SSL_KEY}" ]]; then |
| echo "ERROR: HTTPS certificate or key is missing." >&2 |
| echo "Expected SSL_CERT=${SSL_CERT} and SSL_KEY=${SSL_KEY}" >&2 |
| exit 1 |
| fi |
| ensure_tls |
|
|
| if [[ ! -d "${MODEL_DIR}" ]]; then |
| echo "ERROR: AX_MODEL_DIR does not exist: ${MODEL_DIR}" >&2 |
| exit 1 |
| fi |
| MODEL_DIR="$(cd "${MODEL_DIR}" && pwd -P)" |
| |
| |
| if ! "${PYTHON_BIN}" "${ROOT_DIR}/tools/verify_assets.py" \ |
| --manifest "${ROOT_DIR}/src/ax_meeting_cpp/resources/model_assets.json" \ |
| --root "${MODEL_DIR}"; then |
| echo "Fix: deploy all VAD, CAM++, SenseVoice, FireRed AED, and Punc assets under AX_MODEL_DIR." >&2 |
| exit 1 |
| fi |
|
|
| export AX_MODEL_DIR="${MODEL_DIR}" |
| export ASR_BACKEND |
| export HOST="${BIND_HOST}" |
| export PORT |
| if [[ "${ACCELERATOR}" == axcl ]]; then |
| export LD_LIBRARY_PATH="${AXCL_LIBRARY_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" |
| else |
| export LD_LIBRARY_PATH="${BSP_MSP_DIR}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" |
| fi |
|
|
| ensure_runtime_dependencies |
|
|
| WHEEL_PATH="${AXMEETING_WHEEL:-}" |
| wheel_candidates=() |
| if [[ -n "${WHEEL_PATH}" ]]; then |
| if [[ ! -f "${WHEEL_PATH}" ]]; then |
| echo "ERROR: AXMEETING_WHEEL does not exist: ${WHEEL_PATH}" >&2 |
| exit 1 |
| fi |
| if ! selected_wheel="$(select_compatible_wheel "${WHEEL_PATH}")"; then |
| echo "ERROR: AXMEETING_WHEEL is not compatible with ${PYTHON_BIN}: ${WHEEL_PATH}" >&2 |
| echo "Choose a wheel matching the current Python/ABI/platform, or unset AXMEETING_WHEEL for auto-selection." >&2 |
| exit 1 |
| fi |
| WHEEL_PATH="${selected_wheel}" |
| else |
| shopt -s nullglob |
| wheel_candidates=("${ROOT_DIR}"/wheel/ax_meeting_studio-*.whl) |
| shopt -u nullglob |
| if ! WHEEL_PATH="$(select_compatible_wheel "${wheel_candidates[@]}")"; then |
| if (( ${#wheel_candidates[@]} == 0 )); then |
| echo "No AX Meeting wheel was found under ${ROOT_DIR}/wheel/." >&2 |
| else |
| echo "No wheel under ${ROOT_DIR}/wheel/ is compatible with ${PYTHON_BIN}; incompatible wheels were ignored." >&2 |
| fi |
| if [[ "${BUILD_IF_MISSING}" == "ON" && "${CHECK_ONLY}" != true ]]; then |
| if [[ "${ACCELERATOR}" == axcl ]]; then |
| BUILD_SCRIPT="${AXMEETING_BUILD_SCRIPT:-${ROOT_DIR}/build_ax8850_aarch64_wheel.sh}" |
| else |
| BUILD_SCRIPT="${AXMEETING_BUILD_SCRIPT:-${ROOT_DIR}/build_ax650_wheel.sh}" |
| fi |
| if [[ ! -x "${BUILD_SCRIPT}" ]]; then |
| echo "ERROR: AXMEETING_BUILD_IF_MISSING=ON but build script is unavailable: ${BUILD_SCRIPT}" >&2 |
| echo "Copy a wheel into ${ROOT_DIR}/wheel or set AXMEETING_BUILD_SCRIPT." >&2 |
| exit 1 |
| fi |
| echo "Wheel is absent or incompatible; running ${BUILD_SCRIPT}..." |
| "${BUILD_SCRIPT}" |
| shopt -s nullglob |
| wheel_candidates=("${ROOT_DIR}"/wheel/ax_meeting_studio-*.whl) |
| shopt -u nullglob |
| if ! WHEEL_PATH="$(select_compatible_wheel "${wheel_candidates[@]}")"; then |
| WHEEL_PATH="" |
| fi |
| fi |
| fi |
| fi |
| if [[ -z "${WHEEL_PATH}" || ! -f "${WHEEL_PATH}" ]]; then |
| echo "ERROR: no compatible AX Meeting wheel is available for ${PYTHON_BIN}." >&2 |
| echo "Build or copy a matching wheel into ${ROOT_DIR}/wheel/, or set AXMEETING_WHEEL=/path/to/compatible.whl." >&2 |
| exit 1 |
| fi |
|
|
| PYTHON_TAG="$("${PYTHON_BIN}" -c 'import sys; print(f"cp{sys.version_info.major}{sys.version_info.minor}")')" |
| WHEEL_MARKER="${STATE_ROOT}/${PYTHON_TAG}/installed-wheel.sha256" |
| WHEEL_DIGEST="$(wheel_digest "${WHEEL_PATH}")" |
| INSTALLED_DIGEST="" |
| if [[ -r "${WHEEL_MARKER}" ]]; then |
| read -r INSTALLED_DIGEST < "${WHEEL_MARKER}" || true |
| fi |
| needs_wheel_install=false |
| if [[ "${INSTALL_WHEEL}" == "ON" ]]; then |
| needs_wheel_install=true |
| elif [[ "${INSTALL_WHEEL}" == "AUTO" ]] && { [[ "${INSTALLED_DIGEST:-}" != "${WHEEL_DIGEST}" ]] || ! installed_web_package_available; }; then |
| needs_wheel_install=true |
| fi |
| if [[ "${INSTALL_WHEEL}" == "OFF" ]] && ! installed_web_package_available; then |
| echo "ERROR: ax-meeting-studio is unavailable and AXMEETING_INSTALL_WHEEL=OFF." >&2 |
| exit 1 |
| fi |
| if [[ "${CHECK_ONLY}" == true && "${needs_wheel_install}" == true ]]; then |
| echo "ERROR: current wheel is not installed for ${PYTHON_BIN}; preflight will not modify Python packages." >&2 |
| echo "Run ./run_web_studio.sh once, or set AXMEETING_INSTALL_WHEEL=ON." >&2 |
| exit 1 |
| fi |
| if [[ "${needs_wheel_install}" == true ]]; then |
| echo "Installing current AX Meeting wheel..." |
| "${PYTHON_BIN}" -m pip install --user --force-reinstall --no-deps "${WHEEL_PATH}" |
| mkdir -p "$(dirname "${WHEEL_MARKER}")" |
| printf '%s\n' "${WHEEL_DIGEST}" > "${WHEEL_MARKER}.tmp" |
| mv "${WHEEL_MARKER}.tmp" "${WHEEL_MARKER}" |
| fi |
| if ! installed_web_package_available; then |
| echo "ERROR: ax-meeting-studio cannot be imported by ${PYTHON_BIN} after installation." >&2 |
| exit 1 |
| fi |
|
|
| if [[ "${CHECK_ONLY}" == true ]]; then |
| echo "AX Meeting Studio preflight passed." |
| exit 0 |
| fi |
|
|
| DISPLAY_HOST="$(display_host)" |
| echo |
| echo "AX Meeting Studio" |
| echo " URL: https://${DISPLAY_HOST}:${PORT}" |
| echo " Bind: ${BIND_HOST}:${PORT} (single ${ACCELERATOR} worker)" |
| echo " Backends: SenseVoice + FireRed AED/Punc" |
| echo " Models: ${AX_MODEL_DIR}" |
| echo " Wheel: ${WHEEL_PATH}" |
| echo " Logs: uvicorn=${AXMEETING_UVICORN_LOG_LEVEL}, access=${AXMEETING_ACCESS_LOG}, ax=${AXMEETING_AX_LOG_LEVEL}" |
| echo |
| exec "${PYTHON_BIN}" -m ax_meeting_studio.web.server |
|
|