Docs / Offline & Kiosk Deployment

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:

  1. Pre-Bundled StreamingAssets Mode (Recommended for Kiosks): Models ship directly inside the standalone game installer. The application is playable immediately upon launch with zero setup.
  2. Offline Local Cache Staging: Models are copied to the machine's LocalApplicationData or persistentDataPath directory 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

  1. Select your NpcRuntimeConfig asset in the Unity Project window.
  2. In the Inspector, set Model Storage Mode to:
    StreamingAssets
  3. 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:

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.


5. First-Run Health Check & Auto-Recovery

For unattended kiosks running 24/7:

  1. Microphone Unplugged Recovery: LocalNPC automatically polls for audio device disconnection every 2 seconds. If a visitor pulls the microphone cable, MicrophoneCapture fires OnDeviceLost and re-attaches gracefully as soon as the device is reconnected (OnDeviceRecovered).
  2. Watchdog Heartbeat: Query _agent.IsReady and _agent.State. If the kiosk is left in Speaking or Thinking state for longer than 30 seconds due to an unforeseen hardware fault, invoke _agent.Interrupt() and _agent.ResetConversation().
  3. 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;
        }
    }

This page is the file docs/09-offline-kiosk-deployment.md of the LocalNPC package documentation (also in the package as Documentation~/09-offline-kiosk-deployment.md).