Offline & Kiosk Deployment
This guide covers deploying LocalNPC in air-gapped, offline, kiosk, and museum exhibit environments where internet access is unavailable or prohibited.
1. Overview & Architecture
LocalNPC runs on the PC: once the models and libraries are on the local disk, LocalNPC makes no network calls (checked by reading the code; no network capture was made). Before that, the model files come from Hugging Face (huggingface.co), over HTTPS with the server's certificate checked, and each is checked with SHA-256; LocalNPC runs no server of its own. For an air-gapped kiosk, download the files on a networked machine (Setup Wizard or Model Manager) and copy them to the kiosk. Voice recognition (STT), reasoning (LLM) and voice activity detection (VAD) execute completely in-memory on the CPU. LocalNPC v1.0 uses no GPU: only CPU libraries ship. Voice synthesis (TTS) uses a Windows system voice installed on the machine; LocalNPC downloads no voice. Whether Windows itself uses the network for these voices has not been checked.
For production kiosks, trade show booths, and air-gapped enterprise machines, you have two provisioning strategies:
- Pre-Bundled
StreamingAssetsMode (Recommended for Kiosks): Models ship directly inside the standalone game installer. The application is playable immediately upon launch with zero setup. - Offline Local Cache Staging: Models are copied to the machine's
LocalApplicationDataorpersistentDataPathdirectory during OS/hardware imaging.
2. Pre-Bundling Models in StreamingAssets
To bundle models directly into your standalone build:
Step 1: Copy Models to StreamingAssets/LocalNPC/
Place your model files into the Unity project's Assets/StreamingAssets/LocalNPC/ folder matching the active runtime preset (e.g., Balanced, the default):
Assets/
└── StreamingAssets/
└── LocalNPC/
├── qwen2.5-1.5b-instruct-q4_k_m.gguf
└── ggml-tiny.en.bin
Note
No voice file goes into StreamingAssets. The NPC speaks with a Windows system voice, so every kiosk PC needs Windows 10 or 11 with a voice for the NPC's language (Settings > Time & language > Speech). Without one the NPC answers in text only and logs one warning. No voice activity model is needed: v1.0 detects speech with the built-in energy VAD.
In StreamingAssets mode, the language model in the download folder is used first if one is there. For a kiosk, keep only the bundled copy.
Step 2: Configure NpcRuntimeConfig
- Select your
NpcRuntimeConfigasset in the Unity Project window. - In the Inspector, set Model Storage Mode to:
StreamingAssets - The build pipeline will automatically validate the presence and checksums of these files before compiling.
Step 3: Automated Build Verification
LocalNPC includes a build preprocessor (BuildPreprocessor.cs) that validates:
- Architecture is set to 64-bit Windows (
StandaloneWindows64). - The
StreamingAssets/LocalNPCfolder contains non-zero byte models matching the active preset. - All eight native libraries (the two
llamalib_win-x64_*.dllfiles for the language model, the five whisper.cpp files for speech-to-text andlocalnpc_winspeech.dllfor the voice) are present and have the SHA-256 the package was tested with.
If any required model is missing from StreamingAssets, the build fails immediately in the Unity Editor with actionable remediation instructions, preventing broken kiosk builds.
3. Post-Build File Structure for Kiosks
When you build a Standalone Windows player, the postprocessor (BuildPostprocessor.cs) adds a build manifest next to the executable. The build folder then looks like this:
BuildDirectory/
├── YourGame.exe
├── LocalNPC_BuildManifest.json <-- Audit trail & SHA-256 model checksum manifest
├── UnityPlayer.dll
├── MonoBleedingEdge/
└── YourGame_Data/
├── Plugins/
│ └── x86_64/
│ ├── llamalib_win-x64_avx2.dll <-- Language model, loaded on CPUs with AVX2
│ ├── llamalib_win-x64_noavx.dll <-- Language model, loaded on CPUs without AVX2
│ ├── ggml-base.dll <-- Speech-to-text (whisper.cpp 1.9.5)
│ ├── ggml.dll <-- Speech-to-text
│ ├── whisper.dll <-- Speech-to-text
│ ├── ggml-cpu-haswell.dll <-- Speech-to-text, loaded on CPUs with AVX2
│ ├── ggml-cpu-x64.dll <-- Speech-to-text, loaded on other CPUs (slow)
│ └── localnpc_winspeech.dll <-- Voice (Windows system voices)
└── StreamingAssets/
└── LocalNPC/
├── qwen2.5-1.5b-instruct-q4_k_m.gguf
└── ggml-tiny.en.bin
Build Manifest Verification
The generated LocalNPC_BuildManifest.json provides an exact checksum audit trail:
{
"package": {
"name": "com.localnpc.core",
"version": "1.0.0",
"buildGuideVersion": "4.0"
},
"buildSummary": {
"buildDateUtc": "2026-10-15T12:00:00.0000000Z",
"platform": "StandaloneWindows64",
"unityVersion": "6000.3.10f1",
"executablePath": "C:/Builds/Kiosk/YourGame.exe",
"scriptingBackend": "Mono2x",
"allowUnsafeCode": false
},
"nativePlugins": [
{ "filename": "llamalib_win-x64_avx2.dll", "fileSizeBytes": 13541888, "sha256": "e1f82db0e2883ba6a21420715f00c9e1de57daff89e150073e8b6979ec93dc91" },
{ "filename": "llamalib_win-x64_noavx.dll", "fileSizeBytes": 13371392, "sha256": "9ab768752148f4d28632683f6fb4efa475a8ee2d1d55813215f82c1bb7b25d33" },
{ "filename": "ggml-base.dll", "fileSizeBytes": 693248, "sha256": "970abcbe367a31f03182725a3e7acc90b0974dc34bdec4c220ef8e170e55a881" },
{ "filename": "ggml.dll", "fileSizeBytes": 62464, "sha256": "f0f940e6789651726fcf7d377d2c733cdf36df2abdcf4b55c57cc9dd7b418d4f" },
{ "filename": "whisper.dll", "fileSizeBytes": 1370112, "sha256": "6089110fe007dcb650fc4578232adfcf06316e7dc7eabefd7e1a74e1bcd39fa7" },
{ "filename": "ggml-cpu-haswell.dll", "fileSizeBytes": 1010688, "sha256": "5c9451f58c46069d831da00ca04ea68113411bec53b2a5bff747ad58ce6cc3bd" },
{ "filename": "ggml-cpu-x64.dll", "fileSizeBytes": 866816, "sha256": "b6bb63272a9eb1d9e0855fa5ca8c432e2e40c6fba15a73c50bff5217c68ea4fa" },
{ "filename": "localnpc_winspeech.dll", "fileSizeBytes": 196608, "sha256": "5c506198094011f944c0f4e178a4e0cb3e196684c56cf3c9e81f668b2069bc1f" }
],
"streamingAssetsModels": [
{ "filename": "qwen2.5-1.5b-instruct-q4_k_m.gguf", "fileSizeBytes": 1117320736, "sha256": "6a1a..." }
],
"configuredRuntimePresets": [
{ "assetName": "KioskRuntimeConfig", "preset": "Balanced", "storageMode": "StreamingAssets", "threads": 0 }
]
}
The sha256 of a native library is the pin the package checks at load time: SHA-256 without the parts that Authenticode signing changes, so a library you sign keeps it. The sha256 of a model is the SHA-256 of the whole file. streamingAssetsModels lists the files directly in StreamingAssets/LocalNPC, and configuredRuntimePresets lists every NpcRuntimeConfig asset in the project.
4. Kiosk Interaction Design & Ambient Noise
In public venues (museums, trade shows, retail kiosks), background crowd noise and echo can falsely trigger an open-microphone Voice Activity Detector (VAD).
Listening in v1.0: the Built-In Microphone
In v1.0 a kiosk listens like any other scene: NpcAgent.StartListening() starts the agent's built-in microphone, and an utterance ends after 900 ms of silence. The 03_Kiosk sample works this way: the visitor clicks Start Voice and asks.
The microphone's noise settings (speech threshold, 180 ms minimum speech, 900 ms of silence, 500 ms pre-roll, input gain) and the 150 ms echo tail are fixed in the code. They were tuned by ear in one room only, not in a crowded or loud venue. Test them in your venue before you deploy.
Push-to-Talk: Not Connected in v1.0
PushToTalkController exists, but in v1.0 it is not connected to the microphone. It records only the audio that your own code passes to PushAudioFrame, so holding its button sends nothing to the NPC. Connecting it to the microphone is planned for v1.1. The API, for reference:
using UnityEngine;
using LocalNPC;
using LocalNPC.Audio;
public class KioskPhysicalButton : MonoBehaviour
{
[SerializeField] private NpcAgent _agent;
[SerializeField] private PushToTalkController _ptt;
// Connect to physical arcade button, arcade encoder, or touch button
public void OnPhysicalButtonPressed()
{
_ptt.SetPressed(true);
}
public void OnPhysicalButtonReleased()
{
_ptt.SetPressed(false);
}
}
Physical Microphone Selection
For a kiosk we recommend a headset or a directional microphone (cardioid or gooseneck) aimed at the visitor's mouth. In v1.0 the microphone listens without a button, so the microphone is your main protection against crowd noise and against the NPC hearing its own voice from the speakers.
- Avoid omnidirectional array microphones built into kiosk monitors, as they capture ambient reflections.
- In Windows Sound Settings, disable automatic gain control (AGC) and set the microphone input level by ear (no level was tested).
5. First-Run Health Check & Auto-Recovery
For unattended kiosks running 24/7:
- Microphone Unplugged Recovery:
LocalNPCautomatically polls for audio device disconnection every 2 seconds. If a visitor pulls the microphone cable,MicrophoneCapturefiresOnDeviceLostand re-attaches gracefully as soon as the device is reconnected (OnDeviceRecovered). - Watchdog Heartbeat: Query
_agent.IsReadyand_agent.State. If the kiosk is left inSpeakingorThinkingstate for longer than 30 seconds due to an unforeseen hardware fault, invoke_agent.Interrupt()and_agent.ResetConversation(). - Session Reset Timeout: Use a countdown timer to reset conversation history when no visitor has spoken to the NPC for 60 seconds:
void Update() { if (Time.time - _lastInteractionTime > 60f && _agent.State == NpcState.Idle) { _agent.ResetConversation(); _lastInteractionTime = Time.time; } }