<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nishchal Singh</title>
    <description>The latest articles on DEV Community by Nishchal Singh (@nishchal-29).</description>
    <link>https://dev.to/nishchal-29</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3554529%2F8bdb6ecd-00c2-4786-b153-335664538555.jpg</url>
      <title>DEV Community: Nishchal Singh</title>
      <link>https://dev.to/nishchal-29</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nishchal-29"/>
    <language>en</language>
    <item>
      <title>GhostRunner: Turning Every Street into a Story</title>
      <dc:creator>Nishchal Singh</dc:creator>
      <pubDate>Thu, 15 Oct 2026 21:42:48 +0000</pubDate>
      <link>https://dev.to/nishchal-29/ghostrunner-turning-every-street-into-a-story-5g9o</link>
      <guid>https://dev.to/nishchal-29/ghostrunner-turning-every-street-into-a-story-5g9o</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GhostRunner&lt;/strong&gt;: &lt;em&gt;An open-source, local-first audio drama engine that turns the real streets under your feet into the set of a thriller, and a one-laptop side hustle for anyone who wants to sell themed run and walking experiences.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;GhostRunner is a generative audio companion for runners and walkers. You pick a genre (zombie escape, cyberpunk courier, spy exfiltration), a distance and an intensity. GhostRunner plans a route over real streets, finds the real landmarks along it, and writes an audio drama in which &lt;strong&gt;your surroundings are the set&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Turn right onto Oak Street. See that old brick building on your left? That's where the drop is hidden."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then you put your phone away and run. The right line plays at the right place, in a radio-dispatcher voice, just before the turn it describes. If you speed up, the tone turns urgent and a pursuer closes in. If you flag, the story hands you a breather. If you take a wrong turn, the dispatcher notices, re-routes you and rewrites the rest of the mission.&lt;/p&gt;

&lt;p&gt;Three things make it more than a prompt wrapped around a map:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is spatially exact.&lt;/strong&gt; Every beat is anchored to a distance along the route, so a line about a turn plays before the turn, not after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is grounded.&lt;/strong&gt; The narrator can only name landmarks that exist in OpenStreetMap along your route. A validator rejects anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is fast, because nothing is generated while you run.&lt;/strong&gt; The story is compiled and voiced ahead of you. Playing a line is a lookup, not an inference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it runs on a laptop with a &lt;strong&gt;4 GB GPU&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The side hustle: selling mission packs
&lt;/h3&gt;

&lt;p&gt;The expensive part of an audio drama has always been making it. A writer, a voice actor and a studio for every route does not scale to "your neighbourhood". GhostRunner turns that into a few minutes of laptop time per route, and that makes a small one-person service possible. The unit of sale is a &lt;strong&gt;mission pack&lt;/strong&gt;: a route, a compiled story and voiced audio, made once and replayed any number of times.&lt;/p&gt;

&lt;p&gt;Who I think would pay for one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Running clubs and run-shop events.&lt;/strong&gt; A "Zombie 5K" or "Cyberpunk Courier" themed around the club's own neighbourhood, with the club's real landmarks, for a monthly social run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walking-tour hosts, hotels and campuses.&lt;/strong&gt; A self-guided story tour that uses the venue's real landmarks (I can add them by hand through a &lt;code&gt;demo_pois.json&lt;/code&gt; file) and a branded voice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runners directly.&lt;/strong&gt; City or neighbourhood packs for people who are bored of the same loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be straight about where this stands: I haven't run a paid pilot or validated pricing, and today a saved mission replays from my own machine. The pieces that make it sellable (compile once, replay forever, no per-listener cost, landmarks and voice swapped per client) are built. Exporting a pack as a standalone bundle a customer's phone can play without my server is the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://drive.google.com/file/d/18aRVKpOGKQFeASiO6tCEzBHtIs-Ui2ch/view?usp=drive_link" rel="noopener noreferrer"&gt;Video Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F59qpjdeqrfwnh7u8bknm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F59qpjdeqrfwnh7u8bknm.png" alt=" " width="800" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo3y0z7k0izq1mqb0zlij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo3y0z7k0izq1mqb0zlij.png" alt=" " width="707" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/Nishchal-29/GhostRunner" rel="noopener noreferrer"&gt;https://github.com/Nishchal-29/GhostRunner&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;engine/     routing, chainage math, corridor POIs, tracker, trigger engine, pacing, safety
pipeline/   LangGraph compile_graph and runtime_graph
llm/        Ollama client (structured output)
audio/      TTS providers and clip cache
client/     mobile-first PWA (vanilla JS, Web Audio API, Leaflet)
tools/      LLM benchmark, GPX replayer, demo launcher
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;conda activate agents-env
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;                 &lt;span class="c"&gt;# Valhalla (routing) + Postgres&lt;/span&gt;
python tools/setup_valhalla.py
ollama serve
python tools/demo.py                 &lt;span class="c"&gt;# compiles a demo loop and starts the app&lt;/span&gt;
&lt;span class="c"&gt;# open http://localhost:8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The problem: latency and trust
&lt;/h3&gt;

&lt;p&gt;A story that plays late is wrong, and a story that invents landmarks is worse. A runner moves roughly 3 m/s. An LLM on a small GPU takes seconds per line, and text-to-speech adds more. If you generate reactively, the line about the clocktower arrives after you have passed it. And small models hallucinate confidently.&lt;/p&gt;

&lt;p&gt;So I split the system by &lt;em&gt;when&lt;/em&gt; work happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMPILE (before and during the run)
Plan → Valhalla route → Overpass corridor POIs → route model (chainage)
     → Director (LLM, 1 call) → Beat planner → Beat writer (LLM, 3 variants/beat)
     → Validator → TTS pre-render (rolling window)

RUN (every tick, no LLM on the happy path)
GPS → pace classifier (TabPFN) → locate on route → trigger check → variant select → play
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two LangGraph graphs: &lt;code&gt;compile_graph&lt;/code&gt; carries the heavy work, &lt;code&gt;runtime_graph&lt;/code&gt; is a handful of deterministic nodes that finish in milliseconds. This split is also what makes the business model work: everything expensive happens once, at compile time, and the result is reusable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The route is a coordinate system
&lt;/h3&gt;

&lt;p&gt;I use &lt;strong&gt;Valhalla&lt;/strong&gt; with pedestrian costing for routing. It returns an encoded polyline plus maneuvers with street names. I decode it (precision 6), project into the local &lt;strong&gt;UTM&lt;/strong&gt; zone with &lt;code&gt;pyproj&lt;/code&gt;, and densify to one point every ~5 m.&lt;/p&gt;

&lt;p&gt;Everything on the route is then addressed by &lt;strong&gt;chainage&lt;/strong&gt;: meters along the path. A turn is at 1,240 m. A fountain is at 1,310 m, on the left. This one choice does most of the work in the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triggers are &lt;em&gt;distance-based&lt;/em&gt;, so a pre-compiled beat stays valid at any pace. Pace only selects which variant plays.&lt;/li&gt;
&lt;li&gt;Side-of-route is a cross product in metric coordinates: with tangent &lt;strong&gt;T&lt;/strong&gt; and landmark vector &lt;strong&gt;L&lt;/strong&gt;, the sign of &lt;code&gt;Tx·Ly − Ty·Lx&lt;/code&gt; says left or right.&lt;/li&gt;
&lt;li&gt;Runtime snapping searches only &lt;code&gt;[last_chainage − 30 m, last_chainage + 200 m]&lt;/code&gt; of the densified path, so loops and out-and-back routes can't snap the runner onto the wrong leg.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some details I designed around, because they fail silently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Valhalla's &lt;code&gt;begin_shape_index&lt;/code&gt; is relative to each leg, so multi-leg routes need a running offset or every maneuver lands in the wrong place.&lt;/li&gt;
&lt;li&gt;Street detours make loops overshoot (a triangle at radius &lt;code&gt;D/4&lt;/code&gt; is ~0.93·D before detours). I generate three candidate loops from waypoints at 60°/180°/300°, score them by distance error and self-overlap, and recalibrate the radius once if the first pass is off by more than 15%.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Real landmarks, not imagined ones
&lt;/h3&gt;

&lt;p&gt;One &lt;strong&gt;Overpass&lt;/strong&gt; query along a simplified (≤150 point) buffer of the route pulls historic sites, artwork, fountains, places of worship, named buildings, parks, waterways, bridges and steps. I score each by kind and distance from the path, then apply greedy non-maximum suppression so no two story landmarks sit within 150 m of each other. Results are cached per H3 cell in Postgres, so a second run makes no Overpass calls.&lt;/p&gt;

&lt;p&gt;OSM coverage varies. When a route is landmark-poor, the story degrades gracefully into street names, turns, parks and water, and a hand-curated &lt;code&gt;demo_pois.json&lt;/code&gt; can add landmarks. That file is also how a client's own landmarks (a club's meeting point, a hotel, a campus building) get into a pack.&lt;/p&gt;

&lt;h3&gt;
  
  
  The LLM narrates; it doesn't orchestrate
&lt;/h3&gt;

&lt;p&gt;Small models are unreliable at tool calling, and every tool-call round trip costs latency a runner can't spare. So all decisions are plain Python: which beat goes where, which landmark, what maneuver text, how far ahead to trigger. The LLM does two jobs only:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Director (one structured call):&lt;/strong&gt; produces three acts with chainage ranges, 3-5 objective landmarks (drop, checkpoint, extraction), an antagonist premise, and a &lt;strong&gt;story bible&lt;/strong&gt; (callsigns, motifs, tone).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beat writer:&lt;/strong&gt; for each beat, writes &lt;strong&gt;three variants&lt;/strong&gt; (cruising, accelerating, exhausted) of at most 30 words, from a structured brief. Each brief carries the story bible, a rolling summary of recent beats, and the landmarks already passed, so lines can call back to earlier ones.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A &lt;strong&gt;validator&lt;/strong&gt; enforces word count, an allowlist of landmark names, and safety rules (no crossing roads, no trespassing). On failure it retries once with the violation named, then falls back to a deterministic template.&lt;/p&gt;

&lt;p&gt;The model sits behind a small &lt;code&gt;LLMClient&lt;/code&gt; interface, so swapping it is a one-line config change. That matters for a business: a model or a price I depend on can change without breaking the product.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trigger timing
&lt;/h3&gt;

&lt;p&gt;For each beat, the line should &lt;em&gt;finish&lt;/em&gt; as the runner reaches the anchor. The engine dead-reckons position between 3-5 s GPS fixes and fires a beat when:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;due&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chainage_est&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;est_duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;lead_m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dur&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# finish speaking as the runner arrives
&lt;/span&gt;    &lt;span class="n"&gt;start_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;anchor_chainage_m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lead_m&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;start_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;chainage_est&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_chainage_m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Durations come from the &lt;strong&gt;measured length of the rendered audio clip&lt;/strong&gt;, so the lead distance is accurate to the clip. A priority queue (navigation &amp;gt; checkpoint &amp;gt; landmark &amp;gt; challenge &amp;gt; tension) plays one beat at a time, enforces a minimum gap between non-navigation beats, and skips expired ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pace awareness with TabPFN
&lt;/h3&gt;

&lt;p&gt;I treat pace as a small tabular classification problem. Each tick builds features: rolling speed over 30 s / 2 min / 5 min, speed delta and variance, grade, time since last stop, and current speed relative to a personal baseline from the first two minutes. &lt;strong&gt;TabPFN&lt;/strong&gt; classifies the runner as &lt;code&gt;CRUISING&lt;/code&gt;, &lt;code&gt;ACCELERATING&lt;/code&gt;, &lt;code&gt;EXHAUSTED&lt;/code&gt; or &lt;code&gt;STOPPED&lt;/code&gt;, with a 3-tick hysteresis filter so the story doesn't flip-flop. It runs on CPU, throttled to once every 5 s, leaving all VRAM to the LLM.&lt;/p&gt;

&lt;p&gt;One honest caveat: there is no labeled dataset for this, so the training labels come from a transparent heuristic over simulated run profiles. TabPFN's value here is a fast, calibrated, retrain-in-seconds classifier. If it fails to load, a rules classifier takes over automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Voice
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;ElevenLabs&lt;/strong&gt; voices every line, rendered ahead of the runner through a rolling window of the next three triggers. Clips are content-addressed (&lt;code&gt;sha256(voice + text)&lt;/code&gt;), so repeated lines are free. Reactive moments (off route, pick up the pace, checkpoint countdown) use a pre-rendered &lt;strong&gt;tier-C bank&lt;/strong&gt; that plays from disk in under 50 ms while any dynamic line renders in the background. On the client, Web Audio adds a bandpass-and-distortion radio effect, and the ambient drones and sirens are synthesized procedurally, so there are no audio assets to ship.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the cost actually is
&lt;/h3&gt;

&lt;p&gt;I tried to be honest with myself about the unit economics, because "free" is rarely the whole story.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Story text: local, unmetered.&lt;/strong&gt; One route is roughly 25 beats times 3 variants of about 30 words, so on the order of 7-8k generated tokens. That is cheap even through an API, so I won't pretend the local model is what makes the numbers work. What it buys is that I can recompile, re-benchmark and retune prompts hundreds of times with no meter running and no rate limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing, landmarks and maps: open data, no per-request fee.&lt;/strong&gt; Valhalla runs on my machine. Public Overpass has fair-use limits, so a real service would self-host it, and H3 caching means a route's landmarks are fetched once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice: the one metered cost.&lt;/strong&gt; A full pack with every variant voiced is roughly 25 beats × 3 variants × ~150 characters, so about 11k characters by my estimate (check it against your own plan). Because a pack is rendered once and then replayed, voice cost is &lt;strong&gt;per route, not per listener&lt;/strong&gt;. Content-hash caching also means any repeated line is paid for once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That shape is what makes a side hustle plausible: fixed cost per route, near-zero cost per run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Product details that make it feel real
&lt;/h3&gt;

&lt;p&gt;A mobile-first PWA with a briefing screen (route preview, premise, objectives, safety acknowledgement, headphone check), a countdown, a run HUD, and a debrief with a shareable card. Every compiled mission is saved, so any mission &lt;strong&gt;replays with the LLM, router and TTS all switched off&lt;/strong&gt;. Wi-Fi at a venue, or a trail with no signal, is the one thing a demo or a customer can't count on, so a saved mission needs none of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd build next to actually sell it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Export a mission pack as a self-contained bundle a phone can play with no server.&lt;/li&gt;
&lt;li&gt;Branded packs: a client supplies landmarks, a meeting point and a voice, and the pack is generated from that.&lt;/li&gt;
&lt;li&gt;A second narration language, which the open-model route makes possible without a new vendor.&lt;/li&gt;
&lt;li&gt;A pilot with one local running club to learn what they would really pay for.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stack
&lt;/h3&gt;

&lt;p&gt;Python, FastAPI, WebSockets, LangGraph (Postgres checkpointer), Valhalla, Overpass/OpenStreetMap, shapely, pyproj, H3, Ollama with an open-weight model (&lt;code&gt;[model]&lt;/code&gt;), TabPFN, ElevenLabs, Sentry, Postgres, vanilla JS with the Web Audio API and Leaflet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For a project someone might pay for, the open pieces decide whether the business works at all, not just whether it's a nice demo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Customers' location stays under their control.&lt;/strong&gt; A runner's GPS trail is sensitive, and the people I'd sell to (clubs, schools, employers, tour operators) will ask where it goes. In GhostRunner the live trail never leaves the machine running the app: the model, router and pace classifier are all local, and raw trails are deleted when a session ends. The only things that leave are the planned route corridor (to Overpass, which can be self-hosted) and the script text (to the TTS provider). "We never see your runners' location" is a sentence I can say honestly, and it would be hard to say with a hosted model in the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in on the parts that can change under me.&lt;/strong&gt; The narrator is behind an &lt;code&gt;LLMClient&lt;/code&gt; interface and the voice is behind a &lt;code&gt;TTSProvider&lt;/code&gt; interface, with a mock provider so the full pipeline runs with no vendor at all. I picked the model by benchmark, and I can re-pick it. If a provider changes pricing or retires a model, I change a config value, not the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The niche is a street, and open data fills it.&lt;/strong&gt; The thing a general-purpose API handles worst is "the brick building on &lt;em&gt;this&lt;/em&gt; corner". A model can't know that, however large it is. OpenStreetMap does, and the architecture injects it and then validates the output against it. A client's own landmarks drop in through one file. Open weights also leave fine-tuning a narrator to a client's brand voice on the table, though I haven't done that yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open map data can be cached and reused.&lt;/strong&gt; OSM data is open (ODbL, with attribution required), so I can store routes and landmarks and rebuild packs from them. Many commercial map APIs restrict storing or reusing results, so check the terms of whichever one you would compare against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works with no signal.&lt;/strong&gt; Saved missions replay with the LLM, router and TTS all switched off, which is exactly what a park, a trail or a conference-hall demo needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraints made the design better.&lt;/strong&gt; A 2-4B model won't improvise a perfect story, so I built around it: deterministic structure, grounded briefs, a validator and pre-generation. That same design is what makes a pack reusable and cheap to serve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also want to be straight about the closed piece. Voice is ElevenLabs, because it was the best option for a convincing radio dispatcher and I wasn't going to pretend otherwise. It is isolated behind an interface and cached aggressively, so it is the one swappable, bounded cost instead of something the whole product depends on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of ElevenLabs.&lt;/strong&gt; Every line is voiced in a per-genre radio-dispatcher voice, with an optional second voice for the antagonist. Rolling pre-rendering, content-hash caching and measured clip durations are what let the audio land at the right place, and a pre-rendered reactive bank gives sub-50 ms responses to off-route and pace events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of TabPFN.&lt;/strong&gt; A TabPFN classifier turns streaming telemetry features into a runner state (cruising / accelerating / exhausted / stopped) that selects the story variant in real time, with hysteresis and a rules fallback.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>GhostRunner: Turning Every Street into a Story</title>
      <dc:creator>Nishchal Singh</dc:creator>
      <pubDate>Sun, 11 Oct 2026 21:21:07 +0000</pubDate>
      <link>https://dev.to/nishchal-29/ghostrunner-turning-every-street-into-a-story-50i2</link>
      <guid>https://dev.to/nishchal-29/ghostrunner-turning-every-street-into-a-story-50i2</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GhostRunner&lt;/strong&gt;: &lt;em&gt;An open-source, local-first audio drama engine that turns the real streets under your feet into the set of a thriller.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;GhostRunner is a generative audio companion for runners and walkers. You pick a genre (zombie escape, cyberpunk courier, spy exfiltration), a distance and an intensity. GhostRunner plans a route over real streets, finds the real landmarks along it, and writes an audio drama in which &lt;strong&gt;your surroundings are the set&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Turn right onto Oak Street. See that old brick building on your left? That's where the drop is hidden."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then you put your phone away and run. The right line plays at the right place, in a radio-dispatcher voice, just before the turn it describes. If you speed up, the tone turns urgent and a pursuer closes in. If you flag, the story hands you a breather. If you take a wrong turn, the dispatcher notices, re-routes you and rewrites the rest of the mission.&lt;/p&gt;

&lt;p&gt;Three things make it more than a prompt wrapped around a map:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is spatially exact.&lt;/strong&gt; Every beat is anchored to a distance along the route, so a line about a turn plays before the turn, not after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is grounded.&lt;/strong&gt; The narrator can only name landmarks that exist in OpenStreetMap along your route. A validator rejects anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is fast, because nothing is generated while you run.&lt;/strong&gt; The story is compiled and voiced ahead of you. Playing a line is a lookup, not an inference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it runs on a laptop with a &lt;strong&gt;4 GB GPU&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://drive.google.com/file/d/18aRVKpOGKQFeASiO6tCEzBHtIs-Ui2ch/view?usp=drive_link" rel="noopener noreferrer"&gt;Video Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F59qpjdeqrfwnh7u8bknm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F59qpjdeqrfwnh7u8bknm.png" alt=" " width="800" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo3y0z7k0izq1mqb0zlij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo3y0z7k0izq1mqb0zlij.png" alt=" " width="707" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/Nishchal-29/GhostRunner" rel="noopener noreferrer"&gt;https://github.com/Nishchal-29/GhostRunner&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;engine/     routing, chainage math, corridor POIs, tracker, trigger engine, pacing, safety
pipeline/   LangGraph compile_graph and runtime_graph
llm/        Ollama client (structured output)
audio/      TTS providers and clip cache
client/     mobile-first PWA (vanilla JS, Web Audio API, Leaflet)
tools/      LLM benchmark, GPX replayer, demo launcher
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;conda activate agents-env
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;                 &lt;span class="c"&gt;# Valhalla (routing) + Postgres&lt;/span&gt;
python tools/setup_valhalla.py
ollama serve
python tools/demo.py                 &lt;span class="c"&gt;# compiles a demo loop and starts the app&lt;/span&gt;
&lt;span class="c"&gt;# open http://localhost:8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The problem: latency and trust
&lt;/h3&gt;

&lt;p&gt;A story that plays late is wrong, and a story that invents landmarks is worse. A runner moves roughly 3 m/s. An LLM on a small GPU takes seconds per line, and text-to-speech adds more. If you generate reactively, the line about the clocktower arrives after you have passed it. And small models hallucinate confidently.&lt;/p&gt;

&lt;p&gt;So I split the system by &lt;em&gt;when&lt;/em&gt; work happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMPILE (before and during the run)
Plan → Valhalla route → Overpass corridor POIs → route model (chainage)
     → Director (LLM, 1 call) → Beat planner → Beat writer (LLM, 3 variants/beat)
     → Validator → TTS pre-render (rolling window)

RUN (every tick, no LLM on the happy path)
GPS → pace classifier (TabPFN) → locate on route → trigger check → variant select → play
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two LangGraph graphs: &lt;code&gt;compile_graph&lt;/code&gt; carries the heavy work, &lt;code&gt;runtime_graph&lt;/code&gt; is a handful of deterministic nodes that finish in milliseconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The route is a coordinate system
&lt;/h3&gt;

&lt;p&gt;I use &lt;strong&gt;Valhalla&lt;/strong&gt; with pedestrian costing for routing. It returns an encoded polyline plus maneuvers with street names. I decode it (precision 6), project into the local &lt;strong&gt;UTM&lt;/strong&gt; zone with &lt;code&gt;pyproj&lt;/code&gt;, and densify to one point every ~5 m.&lt;/p&gt;

&lt;p&gt;Everything on the route is then addressed by &lt;strong&gt;chainage&lt;/strong&gt;: meters along the path. A turn is at 1,240 m. A fountain is at 1,310 m, on the left. This one choice does most of the work in the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triggers are &lt;em&gt;distance-based&lt;/em&gt;, so a pre-compiled beat stays valid at any pace. Pace only selects which variant plays.&lt;/li&gt;
&lt;li&gt;Side-of-route is a cross product in metric coordinates: with tangent &lt;strong&gt;T&lt;/strong&gt; and landmark vector &lt;strong&gt;L&lt;/strong&gt;, the sign of &lt;code&gt;Tx·Ly − Ty·Lx&lt;/code&gt; says left or right.&lt;/li&gt;
&lt;li&gt;Runtime snapping searches only &lt;code&gt;[last_chainage − 30 m, last_chainage + 200 m]&lt;/code&gt; of the densified path, so loops and out-and-back routes can't snap the runner onto the wrong leg.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some details I designed around, because they fail silently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Valhalla's &lt;code&gt;begin_shape_index&lt;/code&gt; is relative to each leg, so multi-leg routes need a running offset or every maneuver lands in the wrong place.&lt;/li&gt;
&lt;li&gt;Street detours make loops overshoot (a triangle at radius &lt;code&gt;D/4&lt;/code&gt; is ~0.93·D before detours). I generate three candidate loops from waypoints at 60°/180°/300°, score them by distance error and self-overlap, and recalibrate the radius once if the first pass is off by more than 15%.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Real landmarks, not imagined ones
&lt;/h3&gt;

&lt;p&gt;One &lt;strong&gt;Overpass&lt;/strong&gt; query along a simplified (≤150 point) buffer of the route pulls historic sites, artwork, fountains, places of worship, named buildings, parks, waterways, bridges and steps. I score each by kind and distance from the path, then apply greedy non-maximum suppression so no two story landmarks sit within 150 m of each other. Results are cached per H3 cell in Postgres, so a second run makes no Overpass calls.&lt;/p&gt;

&lt;p&gt;OSM coverage varies. When a route is landmark-poor, the story degrades gracefully into street names, turns, parks and water, and a hand-curated &lt;code&gt;demo_pois.json&lt;/code&gt; can add landmarks.&lt;/p&gt;

&lt;h3&gt;
  
  
  The LLM narrates; it doesn't orchestrate
&lt;/h3&gt;

&lt;p&gt;Small models are unreliable at tool calling, and every tool-call round trip costs latency a runner can't spare. So all decisions are plain Python: which beat goes where, which landmark, what maneuver text, how far ahead to trigger. The LLM does two jobs only:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Director (one structured call):&lt;/strong&gt; produces three acts with chainage ranges, 3-5 objective landmarks (drop, checkpoint, extraction), an antagonist premise, and a &lt;strong&gt;story bible&lt;/strong&gt; (callsigns, motifs, tone).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beat writer:&lt;/strong&gt; for each beat, writes &lt;strong&gt;three variants&lt;/strong&gt; (cruising, accelerating, exhausted) of at most 30 words, from a structured brief. Each brief carries the story bible, a rolling summary of recent beats, and the landmarks already passed, so lines can call back to earlier ones.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A &lt;strong&gt;validator&lt;/strong&gt; enforces word count, an allowlist of landmark names, and safety rules (no crossing roads, no trespassing). On failure it retries once with the violation named, then falls back to a deterministic template.&lt;/p&gt;

&lt;p&gt;On a 4 GB GPU I chose the model by benchmarking rather than reputation: Gemma 2 2B as the baseline against Gemma 4 E2B/E4B and Qwen3 4B, measuring time-to-first-token, tokens per second, and constraint adherence on ten fixed briefs. The winner is &lt;code&gt;[model]&lt;/code&gt;, at &lt;code&gt;[x]&lt;/code&gt; tok/s with &lt;code&gt;[y]&lt;/code&gt;% constraint adherence. &lt;code&gt;[fill in from bench_llm.py]&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Trigger timing
&lt;/h3&gt;

&lt;p&gt;For each beat, the line should &lt;em&gt;finish&lt;/em&gt; as the runner reaches the anchor. The engine dead-reckons position between 3-5 s GPS fixes and fires a beat when:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;due&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chainage_est&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;est_duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;lead_m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dur&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# finish speaking as the runner arrives
&lt;/span&gt;    &lt;span class="n"&gt;start_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;anchor_chainage_m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lead_m&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;start_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;chainage_est&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_chainage_m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Durations come from the &lt;strong&gt;measured length of the rendered audio clip&lt;/strong&gt;, so the lead distance is accurate to the clip. A priority queue (navigation &amp;gt; checkpoint &amp;gt; landmark &amp;gt; challenge &amp;gt; tension) plays one beat at a time, enforces a minimum gap between non-navigation beats, and skips expired ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pace awareness with TabPFN
&lt;/h3&gt;

&lt;p&gt;I treat pace as a small tabular classification problem. Each tick builds features: rolling speed over 30 s / 2 min / 5 min, speed delta and variance, grade, time since last stop, and current speed relative to a personal baseline from the first two minutes. &lt;strong&gt;TabPFN&lt;/strong&gt; classifies the runner as &lt;code&gt;CRUISING&lt;/code&gt;, &lt;code&gt;ACCELERATING&lt;/code&gt;, &lt;code&gt;EXHAUSTED&lt;/code&gt; or &lt;code&gt;STOPPED&lt;/code&gt;, with a 3-tick hysteresis filter so the story doesn't flip-flop. It runs on CPU, throttled to once every 5 s, leaving all VRAM to the LLM.&lt;/p&gt;

&lt;p&gt;One honest caveat: there is no labeled dataset for this, so the training labels come from a transparent heuristic over simulated run profiles. TabPFN's value here is a fast, calibrated, retrain-in-seconds classifier. If it fails to load, a rules classifier takes over automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Voice
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;ElevenLabs&lt;/strong&gt; voices every line, rendered ahead of the runner through a rolling window of the next three triggers. Clips are content-addressed (&lt;code&gt;sha256(voice + text)&lt;/code&gt;), so repeated lines are free. Reactive moments (off route, pick up the pace, checkpoint countdown) use a pre-rendered &lt;strong&gt;tier-C bank&lt;/strong&gt; that plays from disk in under 50 ms while any dynamic line renders in the background. On the client, Web Audio adds a bandpass-and-distortion radio effect, and the ambient drones and sirens are synthesized procedurally, so there are no audio assets to ship.&lt;/p&gt;

&lt;h3&gt;
  
  
  Product details that make it feel real
&lt;/h3&gt;

&lt;p&gt;A mobile-first PWA with a briefing screen (route preview, premise, objectives, safety acknowledgement, headphone check), a countdown, a run HUD, and a debrief with a shareable card. Every compiled mission is saved, so any mission &lt;strong&gt;replays with the LLM, router and TTS all switched off&lt;/strong&gt;. Wi-Fi at a venue is the one thing a demo can't count on, so the whole experience can run without it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stack
&lt;/h3&gt;

&lt;p&gt;Python, FastAPI, WebSockets, LangGraph (Postgres checkpointer), Valhalla, Overpass/OpenStreetMap, shapely, pyproj, H3, Ollama with Gemma, TabPFN, ElevenLabs, Sentry, Postgres, vanilla JS with the Web Audio API and Leaflet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;GhostRunner is built almost entirely on open pieces, and that is not incidental. It is why the product can exist.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open data makes every street a stage.&lt;/strong&gt; OpenStreetMap is a community-built map of the world. Valhalla and Overpass turn it into routes and landmarks. A proprietary map would have limited what I could query, cache and replay, and would have made the offline-replay mode impossible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open weights make location privacy real.&lt;/strong&gt; Your GPS trail is among the most sensitive data a phone produces. Because Gemma runs through Ollama on my own machine, the story is written locally, and the app doesn't send your coordinates to a model provider. Raw trails are deleted when a session ends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open weights make the cost structure work.&lt;/strong&gt; A run generates dozens of lines. With a local model, a 5 km mission costs electricity, not tokens. That is the difference between a toy and something you can offer to anyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small open models reward good engineering.&lt;/strong&gt; A 2-4B model won't improvise a perfect story, which pushed me toward a design that is better for it: deterministic structure, grounded briefs, a validator and pre-generation. Constraints produced the architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open tooling compounds.&lt;/strong&gt; LangGraph, TabPFN, Ollama and Valhalla let one person assemble in days what would otherwise be a platform team's job, and anyone can fork GhostRunner to add a genre, a city, or a language.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of ElevenLabs.&lt;/strong&gt; Every line is voiced in a per-genre radio-dispatcher voice, with an optional second voice for the antagonist. Rolling pre-rendering, content-hash caching and measured clip durations are what let the audio land at the right place, and a pre-rendered reactive bank gives sub-50 ms responses to off-route and pace events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of TabPFN.&lt;/strong&gt; A TabPFN classifier turns streaming telemetry features into a runner state (cruising / accelerating / exhausted / stopped) that selects the story variant in real time, with hysteresis and a rules fallback.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
