Analog Town: A Shortwave Simulator for Fictional Minds

Community Article
Published June 11, 2026

A submission to the Hugging Face Build Small Hackathon — An Adventure in Thousand Token Wood Track

image

Live Space: https://huggingface.co/spaces/build-small-hackathon/analog-town

Model: Qwen/Qwen2.5-7B-Instruct (fallback: Qwen/Qwen2.5-14B-Instruct)

SDK: Gradio · CPU-basic · browser-side TTS, no GPU required

Demo: https://youtu.be/y-Hc-Iu7YBs


The Pitch

You drop a single piece of news into a fictional town — "the old grain silo will be demolished next month" — and the town reacts. Six residents each have their own values, fears, hopes, and grudges. Instead of chatting with them, you tune an analog radio dial through static and hear each resident's inner monologue in a different voice. The next day, you broadcast a follow-up. The town remembers. The map slowly drifts — angry, hopeful, suspicious. That is Analog Town: a perspective-rehearsal tool dressed up as a 1970s shortwave receiver, run by a 7B model with a typed state machine glued behind it.


Why I Built It

Writers, tabletop RPG game masters, classroom debate moderators, and community planners all share the same workflow problem: when something new lands in a fictional community — a factory closing, a sacred grove being bisected, a stranger arriving — they have to mentally simulate every stakeholder's reaction to make the story (or the policy roleplay) feel honest.

The usual fallback is to open a chat window and ask a large model to roleplay each character one by one. That works, but it has three failure modes:

  1. The model collapses every character into the same voice within a few turns.
  2. You get vivid prose but no structured signal about what each character noticed, what they feared, or which value got triggered.
  3. There's no sense of time — every prompt is a one-shot, never a multi-day arc.

We wanted a tool that pushed in the opposite direction: small model, structured output, whimsical interface, time built in. A signal node, not a conversation.


The Core Idea: Minds as Typed State Transitions

Under the hood, Analog Town is not roleplay. Each character is a typed Pydantic state machine, and the model's only job is to compute one well-defined transition:

(Agent Profile + Previous Agent State + Broadcast Event + Day N)
        → Updated Agent State + Internal Monologue + Likely Actions

Every transition returns a strict JSON object (see schemas.py) with fields like:

  • event_summary_from_agent_view — how this character heard it (often a misreading)
  • activated_memory — which private history bubbled up
  • value_conflict — which core value the event collides with
  • emotion_delta — signed changes to trust, anger, fear, hope, curiosity, social_energy
  • updated_state — the new emotional state, clamped to [0, 1]
  • internal_monologue — 1–3 sentences, in their voice
  • likely_private_action / likely_public_action
  • uncertainty — what the model itself isn't sure about
  • safety_note — a fixed disclaimer that this is fictional rehearsal, not prediction

The system prompt is uncompromising: "Your job is not to produce the most dramatic answer. Your job is to produce a plausible, grounded, internally consistent state transition." If the JSON comes back malformed, a dedicated REPAIR_PROMPT rewrites it cleanly.

This is the design lever that lets a 7B model carry a six-character, multi-day simulation: we never ask it to generate freely, we ask it to fill a tightly-shaped form.


What's New This Build (the headline features)

The hackathon push added five differentiators on top of the original single-shot prototype.

🔥 1. Emotion heatmap on the map

After every broadcast, each sprite's avatar ring tints to that character's dominant emotion: red for anger, orange for fear, amber for hope, green for trust, blue for curiosity, purple for social energy. The map becomes a glanceable mood report — you can see at a single look which way each resident bent.

📢 2. Anomaly flags

If any axis of an agent's emotion_delta swings by more than 0.6 in a single broadcast, a pulsing 📢 badge appears on their sprite. Writers and GMs use this to spot dramatic story beats automatically — these are the residents whose minds just lurched.

🎙 3. Browser-side TTS through the radio static

Tune the dial to an agent or click their sprite, and a distinct voice speaks their monologue aloud through the existing ambient-static loop. The static smoothly ducks while the voice plays and swells back up when it ends. Each character gets a deterministically-picked voice from a curated pool of 30+ system voices (male/female, with pitch shifts for elder/young characters), so Margaret the retired stationmaster doesn't sound like Devon the line cook.

We chose browser-side SpeechSynthesisUtterance over an HF Inference TTS endpoint for three reasons: zero latency, zero rate-limit risk, and zero GPU cost — true to the "build small" spirit.

📅 4. Day-N chained broadcasts + timeline

Today's most differentiating feature. Press TRANSMIT once for Day 1. Press it again — agents continue from their post-Day-1 emotional state, with a prompt prefix that tells the model "this is Day 2, do not echo what they said last time." A small horizontal timeline strip shows each broadcast as a clickable pill (DAY 1 · Hotel announcement · ⚠ 2 anomalies). The map heatmap visibly drifts day over day. Click any past pill to time-travel: the map reverts to that day's state, the slider re-tunes through that day's monologues, the day badge updates. A 🔁 RESET button next to TRANSMIT clears the chain back to Day 1.

This turns a one-shot rehearsal into a multi-day arc. Day 1: "the factory will reopen." Day 3: "wages will be 30% lower than last time." Day 5: "first walkout scheduled." Watch each character's mood lurch across the map.

🎨 5. Creative Mode — design your own town

Open the 🎨 CREATIVE MODE accordion, type a 1–3 sentence concept ("a haunted seaside lighthouse where the keeper has vanished and the village is split on whether to investigate"), pick how many residents you want (3–6), and hit 🪄 GENERATE WITH AI. Qwen 2.5 7B drafts a complete, schema-validated Town JSON — full agent dossiers, frequencies, map positions, relationships, and a default broadcast event. You can edit the JSON inline, upload a custom map PNG, then 💾 SAVE & LOAD to register the town and start broadcasting. Frequencies are auto-spread, sprite positions clamped, and the town gets a custom_ prefix so it never overwrites a preset.


The Whimsical Layer: A Police Scanner You Can Tune

The interface is the point. Analog Town is built to feel like the inside of a beat-up radio van parked at the edge of a town you don't live in.

The CRT theme. A custom Gradio CSS theme renders the whole app in amber-and-green phosphor type on pitch-black cards, with scanning-line overlays and glowing horizontal signal-strength bars. IBM Plex Mono for instrument readouts, Crimson Pro for monologues — the typography itself splits "machine talking" from "human talking."

The dial. A frequency slider runs from 87.0 to 108.0 FM. Each character lives on a fixed frequency. As you slide closer to a station, the signal-strength bar climbs, the corresponding character's dossier comes into focus, and their voice fades in through the static.

The static. A loopable 15-second analog hiss bed lives under everything. A small client-side audio manager watches the signal-strength value and smoothly fades the static out when you tune into a station, then back in when you scan between them — and ducks further whenever a voice is speaking.

The map. Nine town presets each ship with a custom 2D isometric pixel-art background, each representing a unique narrative conflict:

  • Graybridge — a foggy mill town arguing about a luxury hotel conversion
  • Neighborhood Council — a suburban block meeting about a 5G tower
  • Port Whisper — a coastal village debating an offshore wind farm
  • Brimstone Hollow — a desert outpost rattled by an unexplained sinkhole
  • Echo Summit — a polar research station picking up a strange signal
  • Rustwood — a rust-belt town whose factory just announced reopening
  • Tin Lantern Junction — a sun-bleached Route 66 stopover town facing an autonomous-truck depot
  • Lotus Wharf — a SE Asian floating river-market village threatened by a bridge that bypasses the market
  • Verdant Spire — a near-future solarpunk vertical city where a bonded municipal AI has just filed for mayor

Characters are placed on each map as circular pixel-art tokens. Clicking a token tunes the receiver, swaps in their dossier, and starts speaking their latest intercepted thought.


A Walkthrough: Three Days, One Town

You load Port Whisper — a coastal fishing village. Four residents, one foggy harbor map. The broadcast box pre-fills with the town's default scenario:

Day 1: "The state energy department has approved the construction of a 40-turbine offshore wind array, 5 miles off the Port Whisper coast."

You hit TRANSMIT. Four model calls run, four typed transitions come back. The map sprites tint: Captain Arthur (the elder fisherman) goes red, Mayor Sarah (the pragmatist) goes amber, Chloe (the marine biologist) goes blue. A 📢 badge pulses on Captain Arthur — his emotional delta exceeded 0.6.

You tune to 89.1 MHz — the static fades and Captain Arthur's voice comes through:

"They're putting iron in my ocean. They didn't ask. They didn't visit. They never do."

You change the broadcast text for Day 2 to "The state has scheduled a public hearing at the harbor next Friday and is offering union-scale jobs in turbine maintenance." You TRANSMIT again. Now the prompt carries DAY 2, the temperature bumps up slightly, and the explicit instruction tells the model don't echo prior beats.

The map redraws. Captain Arthur is still red but the anomaly badge is gone (his delta this round was smaller). Mayor Sarah turned green — trust climbing. A new DAY 2 pill appears in the timeline strip and the map's day badge updates.

You tune back to Arthur:

"A hearing. I've sat through a hundred hearings. But maintenance jobs — that's the first thing they've said that sounds like remembering us."

You click the DAY 1 pill — the map snaps back to Day 1's heatmap and Arthur's Day 1 voice returns. Click DAY 2 to come back. Press 🔁 RESET to clear the chain.


Built Small, On Purpose

Analog Town was engineered against the Build Small Hackathon constraints from the first commit:

  1. Models under 32B. We use Qwen/Qwen2.5-7B-Instruct as the primary inference target through the Hugging Face Inference Client, with Qwen/Qwen2.5-14B-Instruct as an automatic fallback if rate limits hit.
  2. Structured generation beats brute force. Because every model call is shaped by a Pydantic schema and a strict system prompt, we run a full town reaction (4–6 characters) on a small model without losing coherence. We keep max_tokens=900 and temperature=0.3 for Day 1, ramping to ~0.55 for chained days to encourage variation without losing JSON discipline.
  3. Browser-side TTS. We use window.speechSynthesis instead of a server-side TTS endpoint. Zero GPU, zero rate-limits, instant playback, distinct per-character voices.
  4. Client-side audio mixing. Static fade is computed in JavaScript from the signal-strength bars. Voice ducking is handled the same way. Zero server-side audio processing.
  5. Static map assets. Each town's pixel-art map is a single PNG; sprite tokens are static avatars composed on top via CSS. The Gradio app runs comfortably on cpu-basic hardware.
  6. AI-generated towns reuse the same model. Creative Mode's town generation uses the same Qwen 2.5 7B endpoint as the simulator — no separate model, no separate inference path.
  7. Inspectable exports. Every simulation run can be exported as a Hugging Face-compatible dataset (town.json, broadcast_event.json, agent_traces.jsonl, auto-generated dataset card). Naive PII patterns are stripped during export, and a safety note is attached to every transition.

What's Not There (On Purpose)

  • No persistent chat. Each broadcast is one transition. The simulator is a rehearsal, not a relationship.
  • No "predict the real person" mode. The prompts explicitly forbid revealing hidden facts not in the profile, and the system prompt refuses to treat output as factual prediction.
  • No multi-turn voice drift. Because the model never sees its own prior monologue as direct context — only the last structured state — we sidestep the slow voice-collapse problem of long roleplay sessions.

Try It

🎙 Tune in: https://huggingface.co/spaces/build-small-hackathon/analog-town

  1. Click anywhere once (unlocks ambient audio + browser TTS).
  2. Pick a town (or open 🎨 CREATIVE MODE and generate your own).
  3. Type a broadcast.
  4. TRANSMIT. Watch the map tint.
  5. Tune the dial. Listen.
  6. TRANSMIT again. Watch the town drift.

Analog Town is not a chatbot. It's a tiny social weather station for fictional worlds.

Community

Sign up or log in to comment