[KongchangAI]
· 3 min read· 1,783 words

GhostRunner: Turn Every Street Into a Thriller — A Local AI Audio Running Engine

GhostRunner: Turn Every Street Into a Thriller — A Local AI Audio Running Engine

GhostRunner uses a compile/runtime split to turn real streets into an offline interactive audio thriller.

GhostRunner is an open-source, local-first audio drama engine that turns a runner's real route into the stage for a zombie escape or spy extraction radio drama. Its core is a compile/runtime split: before the run, an LLM generates the story, landmarks, and voiceover in one pass; at runtime, only millisecond deterministic lookups and triggers occur. Story beats are anchored to specific meter marks along the route, landmarks come strictly from OpenStreetMap, and a validator rejects any fabricated landmark. The LLM serves only as director and beat writer, with all orchestration in pure Python. Pace is classified with TabPFN into four states to select story variants. Voice is the only usage-based cost, rendered once per route and replayable indefinitely, making a one-person business viable. The whole system runs on a laptop GPU with 4 GB of VRAM.

Listening to podcasts while running just isn't thrilling enough anymore. GhostRunner offers a bolder idea: turn the real streets beneath your feet into the stage for a thriller radio drama. Pick a theme — zombie escape, cyberpunk courier, spy extraction — set your distance and intensity, and the system plans a real route, identifies landmarks along the way, then generates an audio storyline where your surroundings are the set.

According to the developer's submission to the Hacktoberfest open-source AI challenge, GhostRunner is an open-source, local-first audio drama engine. It doesn't just serve as a running companion — the developer also designed it as a one-person business: producing and selling themed running/walking experience packs.

GhostRunner interface screenshot

Core Mechanics: Spatially Precise, Landmark-Grounded, Zero Runtime Inference

What makes GhostRunner more than a "map API wrapper around a prompt" comes down to three design choices:

Spatially precise. Every story beat is anchored to a specific distance along the route. A line about an upcoming turn plays before the turn, not after.

Grounded in reality. The narrator can only reference landmarks that actually exist in OpenStreetMap along the route. A validator rejects any fabricated landmark.

Fast enough. Because the story isn't generated in real time during the run — it's compiled and voiced before execution. Playing a line is a lookup, not an inference.

All of this runs on a laptop GPU with just 4 GB of VRAM.

"Turn right onto Oak Street. See that old brick building on your left? That's where it's hidden."

Routes use "chainage" (meters along the path) as their coordinate system: a turn at 1,240 meters, a fountain at 1,310 meters on the left. This choice makes triggers distance-based rather than time-based, so pre-compiled beats work at any pace.

OpenStreetMap (OSM) is an open-source geographic database maintained by volunteers worldwide. Anyone can freely download, use, and modify its map data. Unlike commercial maps like Google Maps, OSM provides complete raw vector data (roads, buildings, points of interest, trails, etc.) under the ODbL open license. GhostRunner uses the Overpass API to query POIs (points of interest) in OSM along the route corridor — brick buildings, fountains, corner shops, and other landmarks — then feeds them to the LLM as narrative material. This is the foundation of its "grounded in reality" capability: landmarks aren't invented by the model but retrieved from real map data, and the validator builds a whitelist from them, rejecting any landmark name not on the list.

Compile/Runtime Separation: A Solution to Latency and Trust

A runner moves at about 3 meters per second, while an LLM on a small-VRAM GPU takes seconds per sentence, and TTS adds more on top. With reactive generation, a line about the clock tower would arrive after you've already passed it. Small models also confidently hallucinate.

GhostRunner splits the system by "when the work happens":

  • Compile phase (before and during the run): planning → Valhalla routing → Overpass corridor POI → route model (chainage) → director (single LLM call) → beat planner → beat writer (3 variants per beat) → validator → TTS pre-rendering (rolling window)
  • Runtime phase (per tick, no LLM on the normal path): GPS → pace classifier (TabPFN) → route localization → trigger check → variant selection → playback

Two LangGraph graphs divide the work clearly: compile_graph does the heavy lifting, while runtime_graph consists of deterministic nodes that complete in milliseconds. This split is also key to the business model — all expensive operations happen once at compile time, and the results can be reused indefinitely.

LangGraph is an orchestration framework from the LangChain team for building stateful, multi-step AI workflows, modeling tasks as nodes and edges on a graph. In GhostRunner, compile_graph is a heavier graph chaining route planning, POI fetching, director LLM, beat writer, validator, and TTS rendering nodes; runtime_graph is a lightweight graph with almost no model calls, doing only deterministic operations like GPS reading, pace classification, and trigger checks per tick. Splitting them into separate graphs gives the "heavy work" and "fast lookups" different execution frequencies, failure modes, and resource footprints — ensuring responsiveness in a real-time scenario like running.

The LLM Narrates, It Doesn't Orchestrate

Small models are unreliable at tool calling, and each tool-call round trip costs latency a runner can't afford. So all decisions are pure Python: which beat goes where, which landmark to use, what the maneuver text is, how far ahead to trigger. The LLM does only two things:

  1. Director (one structured call): produces three acts (with chainage ranges), 3-5 target landmarks, a villain premise, and a "story bible" (callsign, motifs, tone).
  2. Beat writer: for each beat, writes up to three variants of ≤30 words based on a structured brief (cruising, accelerating, exhausted).

The validator enforces word count, landmark-name whitelists, and safety rules (no jaywalking, no trespassing). On failure, it retries once with violation notes, then falls back to deterministic templates. The model sits behind an LLMClient interface — swapping it is a one-line config change.

Pace Awareness with TabPFN

Pace is treated as a small tabular classification problem. Each tick builds features: 30-second/2-minute/5-minute rolling speed, speed change and variance, gradient, time since last stop, and the ratio of current speed to the personal baseline from the previous two minutes.

TabPFN classifies the runner as CRUISING, ACCELERATING, EXHAUSTED, or STOPPED, with a 3-tick hysteresis filter to prevent the story from flip-flopping. It runs on CPU, throttled to once every 5 seconds, leaving all VRAM for the LLM.

The developer candidly notes one caveat: there's no labeled dataset here — training labels come from transparent heuristics applied to simulated running traces. TabPFN's value lies in being fast, well-calibrated, and retrainable in seconds. If it fails to load, a rule-based classifier takes over automatically.

TabPFN is a Transformer-based tabular data classifier whose key feature is that it can make small-sample predictions without gradient training on your dataset — the model has already been pretrained on a large number of synthetic tabular tasks, and at inference time training samples are fed directly into the forward pass as context, similar to in-context learning in the tabular domain. For scenarios like GhostRunner with no labeled dataset and low feature dimensionality (rolling speed, variance, gradient, etc.), TabPFN produces well-calibrated probability outputs, runs on CPU, and has near-zero retraining cost. A hysteresis filter is a common debounce mechanism in state machines: it only switches when a new state appears stably for several consecutive ticks, preventing the runner from oscillating between two categories and causing story variants to jump frequently.

Voice and Cost: The Only Usage-Based Billing Component

ElevenLabs voices each line, pre-rendered via a rolling window of the next three triggers. Audio clips are content-addressed (sha256(voice + text)), making repeated lines zero-cost. Reactive moments (off-route, speeding up, checkpoint countdown) use a pre-rendered "C-tier reserve pool" played from disk within 50 milliseconds.

The developer offers a candid breakdown of unit economics:

  • Story text: local, unmetered. A route is roughly 25 beats × 3 variants × ~30 words ≈ 7-8k tokens — cheap even via API. What local models actually buy you is "unmetered, unthrottled" freedom to recompile and tune repeatedly.
  • Routing, landmarks, maps: open data, no per-call fees. Valhalla runs locally, public Overpass has fair-use limits (production services should self-host), and H3 caching means landmarks are fetched only once.
  • Voice: the only usage-based cost. A complete pack is roughly 25 beats × 3 variants × ~150 characters ≈ 11k characters. Because a pack is rendered once and replayed indefinitely, voice cost is calculated per route, not per listener.

This cost structure makes a one-person business viable: fixed cost per route, near-zero cost per run.

Content-addressed storage is a storage approach that uses a content hash (here sha256(voice + text)) as the key. Identical content always yields the same address, so deduplication is inherent — repeated lines need neither re-rendering nor re-billing. Rolling-window pre-rendering means always rendering audio for the next several trigger points (e.g., the next three beats) ahead of time at runtime. When the runner actually reaches the trigger distance, the audio is already ready, avoiding the hundreds of milliseconds to seconds of TTS request latency. Combined, these ensure voice cost is incurred only once per route at compile time, not per listener or per playback — the key engineering mechanism behind the "fixed cost per route, near-zero cost per run" unit economics.

Why Open Innovation Matters

For a project that might be paid for, the open parts determine whether the business can succeed.

Customer location data stays under their control. Runner GPS traces are sensitive, and target customers (clubs, schools, employers, tour operators) will ask where the data goes. In GhostRunner, real-time traces never leave the machine running the app, and raw traces are deleted after the session. "We can never see your runners' location" is a claim you can honestly make.

No lock-in on changeable parts. The narrator sits behind an LLMClient interface, voice behind a TTSProvider interface, with mock providers letting the entire pipeline run without any vendor.

The niche is a single street, and open data fills it. What general APIs handle worst is "that corner brick building" — no matter how large the model, it doesn't know, but OpenStreetMap does. Client-owned landmarks can be added via a single demo_pois.json file.

Works without signal. Saved missions replay with the LLM, router, and TTS all turned off — exactly what's needed for demos in parks, trails, or conference halls.

Commercialization Path and Current Status

The unit of sale is a mission pack: one route, one compiled story, and voiced audio — produced once, replayed unlimited times. Potential buyers include running clubs, walking tour operators, hotels, and campuses.

The developer openly admits the current state: no paid pilots or validated pricing yet, and saved missions can currently only be replayed from the local machine. The parts that make the product sellable (compile once, replay forever, no per-listener cost, swap landmarks and voice per client) are already built. Exporting mission packs as standalone packages that play on a customer's phone without a server is the next step.

The community can get the code from the GitHub repository. Running it locally just requires a conda environment plus docker compose to start Valhalla and Postgres, then running python tools/demo.py.

Summary

GhostRunner demonstrates a pragmatic design paradigm: front-load expensive AI work into compile time, leaving runtime to do only deterministic millisecond-scale lookups. Local-first doesn't just solve latency — it also solves privacy, offline usability, and unit economics. It's not yet a validated business, but it clearly proves what's possible when a creator treats the constraints of a 2-4B small model as design conditions rather than defects.

#GhostRunner#AI audio running engine#local-first AI#OpenStreetMap#TabPFN#LangGraph#ElevenLabs#compile runtime separation
Share:

Related articles