Troubleshooting#
Stack Will Not Start#
Confirm the published host ports are free:
ss -ltnp | grep -E "7860|8010|8011|8012|8020"
Confirm Docker Compose can build:
docker compose config docker compose build
Tail individual services to find the first failure:
docker compose logs -f audio-analyzer docker compose logs -f text-to-speech docker compose logs -f rag-service docker compose logs -f kiosk-core docker compose logs -f kiosk-ui
dependency failed to start: container <name> is unhealthy#
Symptom — make up (or docker compose up) aborts with one or more of:
✘ Container audio-analyzer Error dependency audio-analyzer failed to start
✘ Container text-to-speech Error dependency text-to-speech failed to start
dependency failed to start: container text-to-speech is unhealthy
rag-service and kiosk-core gate on depends_on: service_healthy, so a
single service that never reports healthy takes the whole stack down with it.
Find the real error in the failing service’s own log:
docker compose logs -f text-to-speech
docker compose logs -f audio-analyzer
Cause 1 — no route to Hugging Face. The log ends in:
httpx.ConnectError: [Errno 101] Network is unreachable
raised from huggingface_hub during FastAPI startup. The service tried a live
Hub call (model resolution, or an OpenVINO IR export) and the host has no
direct internet access.
Note that setup_models.sh does not pre-fetch these models — it only
handles the OVMS LLM, the queue-analytics YOLO model and Silero VAD. The
audio-analyzer, text-to-speech and rag-service models are downloaded
inside their containers on first run, which is why HF_HUB_OFFLINE=0
(online) is the shipped default in .env.example. Fix with either:
Behind a proxy (first run on a proxied host). Keep
HF_HUB_OFFLINE=0so the models can be fetched. If your proxy is already configured in~/.docker/config.jsonunderproxies.default, Docker injects it for you and there is nothing to do. Otherwise set it in.envand start the stack with the opt-in override — the variables are deliberately not applied without it, so an empty value can never clobber yourconfig.jsonproxy:# .env (uncomment these two lines) KIOSK_HTTP_PROXY=http://proxy.example.com:3128 KIOSK_HTTPS_PROXY=http://proxy.example.com:3128 # optional; defaults to KIOSK_HTTP_PROXY
make up PROXY=true
Exporting them in your shell works too, but only while the two lines in
.envstay commented out — the Makefile does-include .env, and in GNU make a makefile assignment silently overrides the environment, so an uncommentedKIOSK_HTTP_PROXY=would discard your exported value:export KIOSK_HTTP_PROXY="$http_proxy" export KIOSK_HTTPS_PROXY="$https_proxy" make up PROXY=true
Simplest of all: if your shell already has
http_proxy/https_proxyset,PROXY=truepicks them up on its own and you need set nothing:make up PROXY=true
That fallback is deliberately gated on
PROXY=true. Reading the ambient proxy on every run would silently inject each developer’s own proxy — including host-only ones containers cannot reach — into runs that never asked to be proxied. An explicitKIOSK_HTTP_PROXYalways takes priority.If
.envwas created before this behaviour was fixed it may still contain the uncommented empty assignments; comment them out or fill them in. (ThePROXY=truefallback handles that case too — it treats an empty value as unset.)Prefer this over running
docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -dby hand: themaketarget also runscheck-envand keeps the identity/queue profile flags thatmake upnormally adds (queue services are on by default viaQUEUE=true).Intra-cluster traffic always bypasses the proxy via the shared
NO_PROXYanchor indocker-compose.yml.Warm cache, then go offline. Start the stack once on a connected host so each service populates its Hugging Face cache volume, then set offline mode so no Hub call is attempted at all:
HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1
Do not set these on a host that has never completed a successful online run — with an empty cache the services will fail to find their models.
Then recreate the affected services:
docker compose up -d --force-recreate text-to-speech audio-analyzer
Cause 2 — stale image. If the log shows a configuration or missing-file
error rather than a network error, the :latest tag is resolving to an old
cached image. How you refresh it depends on REGISTRY in your .env:
# REGISTRY=true (the shipped default) — images are pulled, not built,
# so --no-cache has no effect. Re-pull instead:
docker compose pull <service-name>
# REGISTRY=false — images are built locally:
docker compose build --no-cache <service-name>
# or, for the whole stack:
make build
Then recreate the service:
docker compose up -d --force-recreate <service-name>
First Startup Is Slow#
This is expected. On first run each model-hosting service downloads or
exports model assets to its models/ directory and Hugging Face cache.
Subsequent starts reuse the cached artifacts. The default
audio-analyzer healthcheck allows up to ~240 seconds for warmup; the
RAG LLM compile on GPU can also take a few minutes the first time.
A health Endpoint Fails#
Run
docker compose psand check theSTATUScolumn forunhealthy.If you are behind a corporate proxy, pass
--noproxy '*'tocurlwhen hitting127.0.0.1.Confirm the service container actually started:
docker compose logs <service-name>
Selected Device Is Not Used#
The device field lives in the per-service pinned config (see Configuration). If the device does not appear in the logs:
Check the value is supported for that model (e.g.,
audio-analyzerASR supportsCPUforprovider: openai, andCPU|GPU|NPUforprovider: openvino—NPUonly works withwhisper-tiny/whisper-base, see ASR Support Matrix).For
GPU: confirm/dev/driexists and the OpenVINO™ GPU runtime is installed.Restart the affected service after the change:
docker compose up -d --build --force-recreate <service-name>
Confirm OpenVINO™ picked the device:
docker compose logs <service-name> | grep -i -E "device|compiling|GPU|CPU"
For audio-analyzer specifically, check the effective provider/device
selection from configs/audio-analyzer/config.yaml:
grep -nE "provider:|device:" configs/audio-analyzer/config.yaml
and verify startup behavior:
docker logs audio-analyzer
docker-compose.yml is the single Compose file and should not override
ASR provider/device. The ASR selection is read only from
configs/audio-analyzer/config.yaml.
Important:
audio-analyzerASR on NPU works only forwhisper-tiny/whisper-base;whisper-small+ fails to compile.make check-envdoes not check model name, so awhisper-small+ NPU config passescheck-envbut crash-loops at container startup. See ASR Support Matrix.
NPU-capable services (queue-service, identity-service face/re-id,
audio-analyzer ASR with whisper-tiny/whisper-base) get the host NPU
device via ACCEL_MOUNT_PATH (defaults to /dev/null so CPU/GPU-only
hosts are unaffected). Neither make up nor make check-env
auto-detects this — export ACCEL_MOUNT_PATH yourself (or set it in
.env) before starting the stack.
Before startup, run:
make check-env
Error: OpenVINO™ does not report an NPU device#
ls -l /dev/accel/
make check-env
For direct Compose runs, set the mapping explicitly:
ACCEL_MOUNT_PATH=/dev/accel/accel0 docker compose up -d queue-service
models.asr.device=NPU Fails to Compile for audio-analyzer#
Provider + Device |
Model |
Result |
|---|---|---|
|
|
✅ Works |
|
|
❌ |
|
any |
✅ Works |
|
any |
❌ Not supported (CPU only) |
Fix: use whisper-tiny/whisper-base on NPU, or switch to GPU/CPU for larger models, then recreate:
docker compose up -d --force-recreate audio-analyzer
docker logs audio-analyzer
curl http://localhost:8010/health
TARGET_DEVICE=NPU — LLM Turns Fail with “Sorry, I encountered an error”#
Symptom. The stack starts, ovms-llm reports AVAILABLE, ASR and TTS
work, but every agent turn — both knowledge questions and ordering
requests — replies Sorry, I encountered an error. Please try again.
Cause. OVMS serves NPU through a Stateful servable (Continuous Batching is CPU/GPU only). The NPU plugin caps prompts at 1024 tokens by default. This agent’s prompt is far larger:
Prompt |
Tokens |
vs 1024 |
|---|---|---|
System instruction only |
~1,530 |
1.5x |
+ 12 MCP tool schemas (a normal turn) |
~3,900 |
3.8x |
+ one tool result (2nd round-trip) |
~4,200 |
4.1x |
Every prompt exceeds the cap, so OVMS rejects the request with
HTTP 400 — Input length exceeds the maximum allowed length. The agent
endpoint swallows the error and surfaces the canned reply.
Warning:
MAX_PROMPT_LENmust be a top-level key ofplugin_config. Written as{"DEVICE_PROPERTIES":{"NPU":{"MAX_PROMPT_LEN":8192}}}it is accepted at load time — the servable still reportsAVAILABLE— but is silently ignored, leaving the 1024 default in force. Verified on MTL with Qwen3-4B: the nested form rejects a 1,824-token prompt, the top-level form serves 3,624. This is recorded only so the next person does not lose a day to it — raising the cap makes NPU work, not usable, for the reasons in the table above.
Recommended fix — use GPU (or CPU). NPU is not supported for the served
LLM. setup_models.sh --device NPU now refuses to run for this reason:
# .env: TARGET_DEVICE=GPU
./setup_models.sh --device GPU
docker compose up -d --force-recreate ovms-llm
The NPU is still used by queue-service, identity-service and
whisper-tiny/whisper-base ASR — pass --skip-ovms to set those up on NPU
while leaving the LLM on GPU.
Why NPU is not offered as an option. Measured on an MTL NPU with Qwen3-4B and OVMS 2026.3, using a graph with the prompt cap correctly raised:
Weight format |
Output quality |
2,559-token tool-calling turn |
|---|---|---|
INT8 (default) |
✅ correct tool call |
801 s |
INT4 ( |
❌ garbage — |
8 s |
INT4 channel-wise ( |
— |
not published for Qwen3-4B (only 8B) |
INT8 latency scales as 191 s (51-token prompt) → 217 s (1,824) → 245 s (3,624) → 801 s once 48 output tokens are generated; decode, not prefill, dominates. A single agent turn issues several such calls, so the only weight format that is correct on NPU is roughly two orders of magnitude too slow for voice.
Other Stateful-servable constraints that apply if this is ever revisited:
NPU uses static shapes, so the full
MAX_PROMPT_LENwindow is compiled into the model — raising the limit costs compile time, memory and latency rather than saving it.Requests are handled strictly one at a time; two concurrent kiosk sessions serialize.
Continuous-Batching flags (
cache_size,max_num_seqs,max_num_batched_tokens) are ignored. Prefix caching is available only viaNPUW_LLM_ENABLE_PREFIX_CACHING.A call can outlast
rag-service’s 90 s generation ceiling (answering.generation_timeout_secs), which produces the same canned error even when OVMS itself would eventually answer.finish_reason=lengthis unsupported, as are beam search,n > 1and logprobs.
Important:
rag-serviceembedding/reranker (RAG_EMBEDDING_DEVICE/RAG_RERANKER_DEVICE, independent ofTARGET_DEVICE) cannot compile on NPU at all — they are exported with dynamic sequence-length shapes, which the NPU compiler rejects. Leave both onCPUorGPU.
IDENTITY_DEVICE=NPU — Face/Re-ID Works, Voice Does Not#
Face/re-id models run on NPU. The voice model (ECAPA-TDNN) fails to
compile (Upper bounds are not specified for node ... compute_STFT), so
voice auth stays disabled (inference_ready=false) — this is expected.
See Identity Service Device.
Permission Errors on Mounted Folders#
Every container runs as UID/GID 1000:1000 (baked into each image).
Model files and caches for audio-analyzer and text-to-speech live
in Docker named volumes (audio_analyzer_models,
audio_analyzer_cache, text_to_speech_models, etc.) initialized with
that ownership, so the usual host-side ownership errors do not apply.
If you still see:
PermissionError: [Errno 13] Permission denied: '...'
on a path inside the container, a named volume was likely created earlier with the wrong ownership (for example by an older root-only run). Reset it:
docker compose down
docker volume rm \
smart-kiosk-assistant_audio_analyzer_models \
smart-kiosk-assistant_audio_analyzer_cache \
smart-kiosk-assistant_text_to_speech_models \
smart-kiosk-assistant_text_to_speech_cache
docker compose up -d
Replace smart-kiosk-assistant_ with whatever Compose project prefix
docker volume ls shows on your host. Resetting a volume forces the
services to re-download model assets on next startup.
Microphone Does Not Work Over a Remote IP (Insecure Origin)#
Browsers only expose navigator.mediaDevices on a secure context —
HTTPS, or a localhost/127.0.0.1 loopback address. When the kiosk stack
runs on a remote or headless machine and you open
http://<remote-ip>:7860 (operator) or http://<remote-ip>:7861
(customer) directly, the page itself loads and renders normally, but every
microphone action fails with:
Microphone access requires HTTPS or localhost.
The UI is not broken and the containers are healthy — the browser is withholding the microphone API because the origin is not trusted. Use either workaround below.
Workaround 1 — SSH port forwarding (recommended)#
Tunnel the UI port so the browser sees a localhost origin, which is
always treated as secure. No browser configuration is needed and it works
in every browser:
# operator UI
ssh -L 7860:localhost:7860 <user>@<remote-ip>
# customer UI (add -L per port, or combine in one command)
ssh -L 7860:localhost:7860 -L 7861:localhost:7861 <user>@<remote-ip>
Leave the SSH session open, then browse to http://127.0.0.1:7860 (or
http://127.0.0.1:7861) on your local machine.
Note: Use
127.0.0.1, not the machine’s LAN IP. Only the loopback address qualifies as a secure origin.
Workaround 2 — Chrome insecure-origin flag#
Tell Chrome to treat the remote origin as secure. This is per-browser and must be repeated on every client machine, so prefer the SSH tunnel for anything beyond a quick demo:
Open
chrome://flags/#unsafely-treat-insecure-origin-as-secure.Add the exact origin, including the scheme and port — for example
http://10.223.23.34:7860. Add a second comma-separated entry forhttp://10.223.23.34:7861if you also need the customer screen.Set the flag to Enabled and relaunch Chrome when prompted.
Warning: This flag disables an origin-security protection for the listed addresses. Use it only on trusted networks, and remove the entry when you are finished.
Browser UI Does Not Capture Audio#
If you are reaching the UI over a remote IP, see Microphone Does Not Work Over a Remote IP first — this is the most common cause.
Confirm the browser granted microphone permission for the origin you are using. Reset the permission and reload if needed.
Check the
kiosk-uilogs for upload errors:docker compose logs -f kiosk-ui
Speaker Labels Are Missing / Diarization Fails to Download#
Speaker diarization pulls three gated Pyannote models from HuggingFace:
Model |
Gated |
Why it is fetched |
|---|---|---|
Yes |
Configured pipeline ( |
|
Yes |
Segmentation dependency of the pipeline |
|
Yes |
Pulled by |
|
|
No |
Embedding dependency — no licence needed |
All three gated repos must be accepted; accepting only the configured
speaker-diarization-3.1 still fails.
Typical audio-analyzer log signature:
401 Client Error ... Cannot access gated repo for url
https://huggingface.co/pyannote/segmentation-3.0/resolve/main/config.yaml
To fix:
Confirm
HF_TOKENis set in.envand the container picked it up:docker compose exec audio-analyzer printenv HF_TOKEN
While signed in with the same HuggingFace account that owns the token, accept the licence on all three pages:
Recreate the service so it retries the download:
docker compose up -d --force-recreate audio-analyzer
If you do not need per-speaker attribution, disable diarization instead — transcription continues to work normally:
# .env
KIOSK_CORE_DIARIZATION_ENABLED=false
Note that a diarizer load failure is non-fatal: it is logged as a
warning and diarization is disabled for that session, so audio-analyzer
stays healthy and /health still passes even when speaker labels are
missing. Always check the logs rather than the health endpoint.
Answer Is Empty or Off-Topic#
Confirm the knowledge base was ingested. The operator UI exposes a Knowledge Base panel; see also rag-service/README.md.
Check
rag-servicelogs for retrieval scores and reranker output.Try the same question from the API to rule out the UI:
curl --noproxy '*' -X POST http://127.0.0.1:8020/api/v1/query \ -H 'Content-Type: application/json' \ -d '{"query":"What are the store hours?"}'
TTS Plays No Audio in the Browser#
Confirm the session snapshot has non-empty
tts_audio_segmentsand notts_errors. See API Reference.The
kiosk-corecontainer and thekiosk-uicontainer share thegenerated_audioDocker volume. If you removed the volume, recreate the stack:docker compose down docker compose up -d --build
kiosk-core Cannot Reach a Downstream Service#
The compose defaults wire kiosk-core and kiosk-ui to the internal
service names (audio-analyzer, text-to-speech, rag-service). If
you override these URLs for a host-run setup, confirm:
The downstream service is reachable from
kiosk-core(trycurlagainst the override URL from inside thekiosk-corecontainer or from the host).For host-run downstreams reached from a container, use
host.docker.internal(see the alternative compose snippets in Run With Docker Compose).