File size: 18,369 Bytes
2c57606 22d2fca 2c57606 22d2fca 2c57606 7ddb9b6 22d2fca 2c57606 22d2fca 2c57606 7ddb9b6 22d2fca 2c57606 7ddb9b6 2c57606 22d2fca 2c57606 7ddb9b6 22d2fca 7ddb9b6 22d2fca 7ddb9b6 22d2fca 7ddb9b6 2c57606 22d2fca 2c57606 22d2fca 2c57606 7ddb9b6 2c57606 7ddb9b6 22d2fca 7ddb9b6 2c57606 7ddb9b6 2c57606 22d2fca 2c57606 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 | #!/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
|