Skip to content
DarshanKrishna-DKPublic

About

The interactive layer for passive stories

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Liminal

Walk into the story you love, and live it.

Liminal is the interactive layer for stories you already love. Streaming gave us an endless shelf of worlds and then left us watching from the outside. Liminal is the threshold you cross to get in. You pick a story, and Liminal rebuilds it as a world you can step inside: one you walk through (Realm) or one you hear and speak to with your eyes closed (Domain). Everything is generated for that story, personalised to you, and always true to the tale the author told.

The Liminal landing page


Contents


What Liminal is

Every story here can be entered, not just watched. You arrive already attached to a world, choose who you want to be inside it, and step through. From there you are not reading a plot, you are living a place: knocking on doors, pressing people who would rather stay quiet, and gathering the pieces of what really happened. The story remembers what you did, and it ends differently depending on the choice you commit to. The one thing it never does is rewrite the author's truth: your path is yours, but the story stays canon.

A generated Realm world


The two modes

Two doors into the same generated world.

  • Realm (visual). A top-down pixel world you actually move through. It is a real open world: each place is a full screen, and walking off an edge takes you to the next place, one screen over. Clue-holders are spread across different places, a fog-of-war minimap fills in as you explore, and locked places open as you listen to the story.
  • Domain (audio). A purely audio experience for eyes closed. The story is spoken aloud in your language while a single wavy orb breathes with every line, and you answer by speaking out loud, hands free. Your choices bend the scene while the source truth stays intact.

The detective board


Architecture

Liminal is one Next.js app. Both modes sit on the same shared spine (catalog, canon, generation, providers), so it is a second face on one engine, not two projects.

flowchart TD
    subgraph Client["Browser"]
      R["Realm<br/>GameCanvas + HUD"]
      D["Domain<br/>WavyOrb + voice loop"]
      P["Reader persona<br/>(localStorage)"]
    end

    subgraph API["Next.js App Router (server)"]
      W["/api/world/:id<br/>generate + stream + cache"]
      DL["/api/dialogue"]
      V["/api/voice · /api/domain/*<br/>tts · transcribe · scene · choice"]
      LR["/api/listen · /api/resolve"]
    end

    subgraph Engine["Shared engine (lib/)"]
      CAT["catalog"]
      CAN["canon + canon-gen"]
      BW["build-world<br/>(screen graph)"]
      SB["domain/scene-brain"]
      PROV["providers:<br/>text · image · voice"]
    end

    subgraph Ext["Providers + storage"]
      GROQ["Groq / OpenAI<br/>(text)"]
      OAI["OpenAI<br/>gpt-image-1 · TTS · Whisper"]
      CACHE["public/generated (art)<br/>.cache/canon (secret, server-only)"]
      SUPA["Supabase<br/>(optional world state)"]
    end

    R --> W & DL & LR
    D --> V
    P -.persona + memory.-> DL & V
    W --> BW --> CAN --> CAT
    V --> SB --> CAN
    DL --> CAN
    BW --> PROV
    SB --> PROV
    PROV --> GROQ & OAI
    W --> CACHE
    LR --> SUPA
    CAN -. secret withheld .- CACHE
Loading

Guardrail: the story's secret (canon.arc.secret) lives only in the server-side canon cache and is woven in only at the committed resolution. It is never sent to the browser on any normal turn.


How a world is generated

Pick a story and press Generate. The engine:

  1. Loads the story's canon, or writes a full canon for stories that have none (places, characters, the hidden truth, and the clues that point to it), from just the catalog entry.
  2. Paints each place in one consistent top-down pixel-art style (so a generated world matches the seeded ones and itself).
  3. Lays the places on a connected screen graph so they spread out and you travel between them by walking, with clue-holders in different places.
  4. Streams progress place by place, then drops you in. Generated worlds are cached so the next visit is instant. It never hangs: any failure offers a retry.

The three demo stories ship with art already made; every other story is built live the first time someone opens it.


The episode and unlock loop

Stories are broken into short episodes (in the spirit of a Pocket FM run). Each episode is a five to ten minute session that opens one new place. You "listen" to the next episode from the drawer and the gated place opens, with a chime and a jolt so an unlock always reads as an event. Binge several episodes to open a larger stretch at once. When you have gathered enough and reached the final place, you commit to a theory and choose how to end it, which produces materially different resolutions.


Personalisation

The problem Liminal solves is personalised interactive experiences: two people who enter the same story should never have the same one.

  • Reader persona. You tell Liminal your name and what pulls you into a story (the truth, the people, or the mystery). It is remembered locally and follows you across stories.
  • Characters who remember you. Each character keeps a private memory of what you have said to them, across sessions, so the next conversation builds on the last. They greet you by name and lean toward or away from you based on your persona, without ever changing the facts.
  • A resolution that is yours. Your persona and the theory you commit to colour the ending, while the source truth stays intact.

Tech stack

  • Next.js 16 (App Router) and React 19, TypeScript throughout.
  • Tailwind CSS v4 (tokens in app/globals.css).
  • A canvas overworld engine (components/GameCanvas.tsx) for movement, the follow/edge camera, and rendering.
  • Swappable provider layer: text (lib/providers/text.ts, Groq or OpenAI), images (lib/providers/image.ts, OpenAI gpt-image-1 with Gemini / Hugging Face fallbacks), voice (lib/providers/voice.ts, OpenAI TTS + Whisper STT).
  • Supabase is optional (saved, resumable worlds); the game plays without it.

Getting started

You need Node 20 or newer and pnpm.

pnpm install
cp .env.example .env.local   # then fill in real values (see below)
pnpm dev

Open http://localhost:3000, set your reader persona, pick a story, and press Generate. The first build of a world takes a minute or two while its places are painted, then it is cached and instant.


Environment variables

Text generation needs one provider; image generation needs one working image provider for any story without pre-made art.

Variable Purpose
LLM_TEXT_PROVIDER openai (default) or groq.
OPEN_AI_API_KEY OpenAI key (note this project's spelling). Used for text, images, TTS, and Whisper.
OPENAI_TEXT_MODEL Defaults to gpt-4o-mini.
OPENAI_IMAGE_MODEL Defaults to gpt-image-1.
OPENAI_TTS_MODEL Defaults to gpt-4o-mini-tts.
OPENAI_STT_MODEL Whisper model, defaults to whisper-1.
GROQ_API_KEY, GROQ_TEXT_MODEL Optional, if using Groq for text.
LLM_IMAGE_PROVIDER openai (recommended), gemini, or huggingface.
SUPABASE_URL, SUPABASE_ANON_KEY Optional. Enables saved, resumable worlds.
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY Optional public Supabase values.

The provider layer tries your preferred image backend first and falls through to any other one that has a working key, so a single good key is enough.


Deployment

The app builds cleanly for Vercel (next build). Two things to know:

  1. Set the environment variables above in the Vercel project settings.
  2. The three seeded stories play fully on Vercel with no generation. Runtime generation of new worlds currently writes art to public/generated/, and Vercel's serverless filesystem is read-only, so live generation on the hosted site needs image writes pointed at Supabase Storage (a follow-up). Locally, generation works end to end.

Project structure

app/                     routes: landing, /stories, /story/[id], /realm/[id], /domain/[id]
  api/world/[storyId]/   dynamic world generation (stream + cache)
  api/dialogue           in-character, canon-true NPC dialogue (+ persona + memory)
  api/voice              OpenAI TTS for spoken lines
  api/domain/            scene, choice, tts, transcribe, tutorial
components/
  GameCanvas.tsx         the open-world canvas engine
  realm/                 RealmWorld, RealmLoader, Minimap, DialogueBox, CharacterSelect
  domain/                WavyOrb, DomainExperience, ChoicePrompt, useVoiceTurn
  site/                  landing + catalog: Hero, ModeSelect, StoryCard, PersonaBar
lib/
  catalog.ts             the story catalog
  canon.ts, canon-types  story canon (seeded + generated)
  world/                 canon-gen, build-world, world-cache, scene-map
  domain/                scene-brain, session, databricks-log
  providers/             text, image, voice
  personalization.ts     reader persona + per-character memory
content/canon/           the three seeded story canons

Built for the stories you already love.

About

The interactive layer for passive stories

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages