playable-lessons
v0.6.4
Published
Turn educational source material into **playable interactive fiction**. Paste in a case study, lecture notes, a methodology, or just a topic, and Playable Lessons uses AI to weave it into a branching [Ink](https://www.inklestudios.com/ink/) story that lea
Maintainers
Readme
Playable Lessons
Turn educational source material into playable interactive fiction. Paste in a case study, lecture notes, a methodology, or just a topic, and Playable Lessons uses AI to weave it into a branching Ink story that learners can play — then export it as a self-contained HTML file, an Ink/Twee source file, or a printable walkthrough. It can also turn the same material into study flashcards, a multiple-choice quiz, a study summary, AI-collaboration tasks, or a teaching case study. A Plan mode recommends which outputs suit your material and can generate them all at once.
It ships in two forms:
- Desktop app (Electron + React) — the full authoring experience: AI generation with an interactive clarification step, a node-graph editor, live player, a Projects dashboard (save / reopen / delete lessons as ownable folders on disk), inline editing with AI refinement (✨), and exports. Stories can be illustrated with an AI-generated image per scene (cartoon or photorealistic) using your own image provider — OpenAI, Google Imagen, or any OpenAI-compatible endpoint. Aimed at non-technical authors (e.g. educators).
- CLI (
playable-lessons, on npm) — the same engine, headless: generate an Ink story, flashcards, a quiz, a summary, AI-collaboration tasks, or a case study; plan a recommended set; validate, and export. Handy for scripts, CI, batch runs, and local/offline generation against Ollama. - Self-hosted web (Docker) — a browser-based version for institutions (e.g. university staff). The server holds the API key; users just enter an access code. No per-user keys, no accounts — work is stored per-browser.
The desktop app and the CLI share one generation pipeline; the app adds the interactive clarification step, visual editor, and inline editing on top.
A public landing page with a try-it demo (bring your own key) is at michael-borck.github.io/playable-lessons.
CLI
Install
npm install -g playable-lessonsGenerate a story from source material
# Local + offline against Ollama (no API key needed):
playable-lessons generate --input notes.md --provider ollama --model llama3.1:8b --output lesson/
# Anthropic Claude (key from the environment):
ANTHROPIC_API_KEY=sk-ant-... playable-lessons generate -i case-study.md -p claude -o lesson/
# From a topic instead of a file, choosing formats:
playable-lessons generate --topic "Phishing awareness for staff" -p openai -o lesson/ -f ink,html,json
# Pipe source material via stdin:
cat lecture.txt | playable-lessons generate -o lesson/ -p ollama -m llama3.1:8b
# With a generated illustration per scene (cartoon or photorealistic):
OPENAI_API_KEY=sk-... playable-lessons generate -i notes.md -p ollama -o lesson/ --images --image-style cartoon--images adds a # IMAGE_PROMPT: tag to every scene, then generates each image with an image provider (--image-provider openai|gemini|custom, inferred from OPENAI_API_KEY/GEMINI_API_KEY/--image-base-url; --image-model to override the default model). Images are embedded in the HTML export and written to an images/ folder.
Provider selection and API keys:
| Provider | --provider | Key (env var) | Notes |
| --- | --- | --- | --- |
| Claude | claude | ANTHROPIC_API_KEY | default model claude-opus-4-8 |
| OpenAI | openai | OPENAI_API_KEY | default model gpt-4o |
| Ollama | ollama | none (or OLLAMA_API_KEY) | local by default; --ollama-url for remote |
| Custom | custom | PLAYABLE_LESSONS_API_KEY / OPENAI_API_KEY | OpenAI-compatible; requires --base-url + --model |
If --provider is omitted it's inferred from the environment (Claude → OpenAI → Ollama). Keys are read from env vars only — the CLI never touches the OS keychain (that's the desktop app). Other flags: --mode, --length (short/medium/long), --tone, --protagonist, --answers, --title, and --target (story | flashcards | quiz | summary | ai-task | case-study — see the per-target sections below). Run playable-lessons generate --help for all options.
Generate flashcards
Add --target flashcards to turn the same source material into a deck of study flashcards instead of an Ink story. Each card has a front, back, optional hint, and optional tag.
# Flashcards from a topic, offline via Ollama (no API key needed):
playable-lessons generate --topic "Supply chain resilience" --target flashcards -p ollama -m llama3.1:8b -o deck/
# From a file, exporting every format and setting the card count:
playable-lessons generate -i notes.md --target flashcards --cards 20 -o deck/ -f csv,html,anki,json| Format | Output |
| ------ | ------ |
| csv | front,back,hint,tag — imports into Anki, Quizlet, Excel (.flashcards.csv) |
| html | Self-contained flip-card deck (.flashcards.html) — click or Space to flip, arrow keys to navigate; works offline |
| anki | Tab-separated import for Anki: front, back, tag (.flashcards.txt) |
| json | The raw structured deck (.flashcards.json) |
The default format for flashcards is csv,html; use --cards N to set the count (default 12). The same source can yield both a story and a deck — run generate twice with different --target.
Generate a quiz
Add --target quiz to produce a self-marking multiple-choice quiz from the same source material. Each question has a stem, 2–6 options, the correct option's index, and an optional explanation.
playable-lessons generate --topic "Supply chain resilience" --target quiz -p ollama -m llama3.1:8b -o quiz/
playable-lessons generate -i notes.md --target quiz --cards 20 -o quiz/ -f html,json,txt| Format | Output |
| ------ | ------ |
| html | Self-contained interactive quiz (.quiz.html) — pick answers, submit for an instant score, with correct/wrong highlighting and explanations; works offline |
| json | The raw structured quiz (.quiz.json) |
| txt | A printable quiz with an answer key (.quiz.txt) |
The default format for quiz is html,json; --cards N sets the number of questions.
Generate a summary
Add --target summary to distill the source material into a study summary: a short overview, the key points, and a glossary of essential terms.
playable-lessons generate --topic "Supply chain resilience" --target summary -p ollama -m llama3.1:8b -o summary/
playable-lessons generate -i notes.md --target summary --cards 10 -o summary/ -f html,txt,json| Format | Output |
| ------ | ------ |
| html | Self-contained study sheet (.summary.html) — overview, numbered key points, glossary; works offline |
| txt | A printable summary with key points and a glossary (.summary.txt) |
| json | The raw structured summary (.summary.json) |
The default format for summary is html,txt; --cards N sets the target number of key points.
Generate AI-collaboration tasks
Add --target ai-task to re-engineer the material into AI-collaboration tasks — tasks a learner completes by iteratively interrogating an LLM chatbot. Each task is built so the chatbot's first answer is wrong or incomplete until the learner probes it: a concrete scenario with 2–4 load-bearing specifics, a student-facing brief, a deliverable, an engagement-anchored rubric (not a correctness rubric), and a note on why a delegating learner loses. (This output never produces quizzes/flashcards/summaries — pick those targets for those.)
playable-lessons generate --topic "clinic outreach on a budget cut" --target ai-task -p ollama -m llama3.1:8b -o tasks/ --cards 3
playable-lessons generate -i worksheet.md --target ai-task -o tasks/ -f html,txt,json| Format | Output |
| ------ | ------ |
| html | Self-contained task sheet (.ai-task.html) — scenario, brief, deliverable, load-bearing specifics, rubric; works offline |
| txt | A printable version of the tasks (.ai-task.txt) |
| json | The raw structured task set (.ai-task.json) |
The default format for ai-task is html,txt; --cards N sets the number of tasks (try --cards 3 for richer tasks).
Generate a case study
Add --target case-study to turn the material into a teaching case study — a realistic situation (protagonist, situation, key facts, conflict, decision points, discussion questions) a learner analyzes. Use --depth to control how much is produced:
| depth | produces |
| --- | --- |
| idea | a brief premise (protagonist, situation, conflict) |
| outline | the full structured skeleton, no prose |
| complete (default) | the full skeleton plus a written narrative |
playable-lessons generate --topic "clinic scheduling under staff shortages" --target case-study --depth complete -p ollama -m llama3.1:8b -o case/
playable-lessons generate -i notes.md --target case-study --depth outline -o case/ -f html,txt,json| Format | Output |
| ------ | ------ |
| html | Self-contained case-study sheet (.case-study.html) — narrative + structured sections; works offline |
| txt | A printable version of the case study (.case-study.txt) |
| json | The raw structured case study (.case-study.json) |
The default format for case-study is html,txt; --depth selects idea | outline | complete (default complete).
Plan a set of outputs (recommender)
Not sure which outputs to make? plan analyzes the material and recommends a complementary set (2–4 of the targets above), then optionally generates them all into the output folder.
# Just recommend a set:
playable-lessons plan --topic "supply chain resilience" -p ollama -m llama3.1:8b -o plan/
# Recommend AND generate every recommended output:
playable-lessons plan -i notes.md --apply -o plan/plan writes plan.json + plan.txt + plan.html (the recommendation). With --apply it also generates each recommended target (.html + .json). In the desktop app, the Plan target shows the recommendation with a “Generate all → new project” action that writes the whole set to a project folder.
Validate an Ink file
playable-lessons validate --input story.inkExport
# Standalone playable HTML
playable-lessons export --input story.ink --output dist/ --format html --title "My Lesson"
# Compiled Ink JSON
playable-lessons export --input story.ink --output dist/ --format json
# Multiple formats at once
playable-lessons export -i story.ink -o dist/ -f html,json,ink| Format | Output |
| ------ | ------ |
| html | Self-contained player (.html) — works offline, share via LMS/email |
| json | Compiled Ink story JSON |
| ink | The raw Ink source (copied through) |
Run playable-lessons --help for all options.
Desktop app (development)
npm install --legacy-peer-deps
npm run dev # launch the Electron app with hot reload
npm run build # production build
npm run build:mac # / build:win / build:linux — package installers
npm run typecheck
npm testAI providers
The desktop app supports Claude, OpenAI, a local Ollama, and any OpenAI-compatible endpoint (remote Ollama, OpenRouter, LiteLLM, vLLM, …) via a base URL + bearer token. Model IDs are editable, with live model listing and a connection test. API keys are stored in your OS keychain (via Electron safeStorage), not in plain config.
Inline editing + AI refinement
After generating any output (flashcards, quiz, summary, AI-collaboration tasks, case study), switch to Study → Edit to tweak the content inline — edit text fields, add/delete items, and use the ✨ button to AI-refine individual fields. Click 💬 before ✨ to give a custom direction (e.g. "simplify for beginners"). Changes flow through to exports.
Self-hosted web (Docker)
For institutions or teams that want a browser-based version without per-user API keys. The server holds the key; users enter a shared access code.
Quick start
# 1. Grab the files
curl -sL https://raw.githubusercontent.com/michael-borck/playable-lessons/main/docker-compose.yml -o docker-compose.yml
mkdir -p server
curl -sL https://raw.githubusercontent.com/michael-borck/playable-lessons/main/server/.env.example -o server/.env
# 2. Edit server/.env — set your API key + optional access code
# ANTHROPIC_API_KEY=sk-ant-...
# ACCESS_CODE=staff-2024 (comma-separated for multiple groups; blank = no gate)
# 3. Swap build for the prebuilt image in docker-compose.yml
# image: ghcr.io/michael-borck/playable-lessons:latest
# 4. Deploy
docker compose pull && docker compose up -dStaff access the server at http://your-server:3000, enter the access code, and generate. Work is stored per-browser (localStorage); the AI key stays server-side. Rate limiting is configurable (RATE_LIMIT_PER_HOUR, default 30) and counts only generation submissions, not status polling.
Generations run as an in-process job queue (one at a time by default; GENERATE_CONCURRENCY to raise). Visitors see their queue position and live pipeline progress, can cancel while waiting (a job already running plays out — the pipeline has no abort switch), and can close the page and come back — results stay fetchable for an hour via a job id in localStorage. Jobs live in memory: a server restart clears the queue, and the UI says so honestly.
To revoke a group: remove their code from .env → docker compose restart. To update: change the image tag → docker compose pull && docker compose up -d.
Scene images (optional)
The hosted app can offer "illustrate every scene" (cartoon or photorealistic) on stories. It's off by default; enable it in .env with SCENE_IMAGES=true plus one image provider:
# Option A — Google Imagen (cheap, fast, ~$0.02/image; good default for a public server):
SCENE_IMAGES=true
GEMINI_API_KEY=AIza...
# Option B — OpenAI (gpt-image-1; reuses OPENAI_API_KEY if IMAGE_API_KEY is blank):
# SCENE_IMAGES=true
# IMAGE_PROVIDER=openai
# Option C — self-hosted generator on the same VPS (LocalAI, included in the compose file):
# docker compose --profile images up -d
# SCENE_IMAGES=true
# IMAGE_PROVIDER=custom
# IMAGE_BASE_URL=http://localai:8080/v1
# IMAGE_MODEL=stablediffusion
# Option D — your own SwarmUI server (GPU, free per image, native API):
# SCENE_IMAGES=true
# IMAGE_PROVIDER=swarmui
# IMAGE_BASE_URL=https://swarmui.example.org
# IMAGE_MODEL=juggernautXL_v9
# IMAGE_API_KEY=... # only if your SwarmUI is behind authNotes for operators:
- Each illustrated story triggers up to
IMAGE_MAX_SCENESimage generations (default 8) — combined with the per-IPRATE_LIMIT_PER_HOUR, that bounds your image spend. - The LocalAI service runs CPU-only by default and is slow (a minute or more per scene on a small VPS). It's internal-only (no published port — LocalAI has no auth). For usable self-hosting, use a GPU host and the
localai/localai:*-gpu-*images; for a CPU-only VPS, prefer Gemini/OpenAI. - First LocalAI start downloads a multi-GB model into the
localai-modelsvolume. - If the app sits behind nginx/Caddy, raise
proxy_read_timeout(e.g.300s) — illustrated stories take minutes — and setTRUST_PROXY=1so the per-IP rate limiter sees real visitor IPs instead of one shared proxy bucket.
Tech stack
Electron + electron-vite · React 19 + TypeScript · Zustand · inkjs (Ink compiler + runtime) · @xyflow/react (graph editor) · yargs (CLI) · Express (self-hosted server).
License
MIT © michael-borck
