AX-Meeting-Studio / run_web_studio.sh
Nnow2024's picture
Upload 8 files
22d2fca verified
Raw
History Blame Contribute Delete
18.4 kB
#!/usr/bin/env bash
# Start AX Meeting Studio on one AX650 board or AX8850 AXCL host. This launcher deliberately
# separates immutable deployment checks from the optional first-run bootstrap.
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}"
# On the target board the BSP msp/out payload is mounted at /soc. Keep this
# configurable for deployments that put the BSP somewhere else.
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}"
# Progress polling is frequent. Keep the board console focused on errors by
# default; these values can be overridden while diagnosing a deployment.
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 # BSP msp/out (include/ and lib/)
AXMEETING_ACCELERATOR=auto|ax650|axcl
AXMEETING_AXCL_DIR=/opt/axcl # custom prefix (lib/libaxcl_rt.so)
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 # optional; otherwise auto-select
AXMEETING_BUILD_IF_MISSING=ON # explicit first-board bootstrap
AXMEETING_INSTALL_DEPS=ON|OFF|AUTO
AXMEETING_INSTALL_WHEEL=ON|OFF|AUTO
AXMEETING_VERIFY_TLS=ON|OFF
--port, -p PORT # overrides PORT for this launch
--check # validate only; never install or start
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
}
# Print the best wheel that can be installed by the current interpreter. A
# deployment directory may contain wheels for several CPython versions and
# platforms; incompatible files are intentionally ignored. packaging is a
# pip dependency on supported Python installations, while the small fallback
# keeps the launcher useful when only the standard library is available.
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)"
# The Web Studio exposes both choices, so validating only ASR_BACKEND would
# allow a later UI switch to fail after the user has already submitted audio.
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