A visual interactive storytelling tool for Game Masters running tabletop RPGs, or any scenario where you need to guide players through a narrative with visual injects.
- Visual Storyline Builder - Create and organize story injects with headings, text, and images
- Real-time Player Sync - Players see updates instantly via WebSocket
- Branching Narratives - Add optional side quests or alternate paths that branch from main storyline
- Day & Time Display - Show story progression with optional day numbers and timestamps
- Auto/Manual Playback - Set durations for automatic progression or advance manually
- Player Type Targeting - Send specific injects to specific player types (e.g., only the Wizard sees magic-related content)
- Silent Targeting - Players see no indication when injects not meant for them are played
- Session Notes - Capture timestamped observations during play with inject references
- Session Clocks - Current time and session stopwatch displayed next to navigation controls
- Inject Preview - Hover over any inject to see a preview of player-visible content and GM notes
- Inject Library - Save and reuse injects across storylines
- Import/Export - Import/export storylines, player types, and library items as JSON
- Zoom Controls - Zoom in/out with auto-fit to show all injects
- Self-Contained Storage - All data including images stored in JSON files for easy backup
- Dark/Light Themes - Both GM and Player interfaces support theme switching
- GM Authentication - Optional password protection for the GM interface
- Responsive Design - Works on desktop and mobile devices
- AI Generation - Included ChatGPT prompt for generating complete storylines
cd storyteller
python app.pyWith GM password protection:
python app.py --password your_secret_password- GM Interface: http://localhost:5000/gm
- Player Interface: Use the Links feature to generate player URLs
- Start the server with
python app.py - Open the GM interface at http://localhost:5000/gm
- Create a new storyline by clicking "+ New"
- Add injects to your storyline using the "+ Add Inject" card
- Activate the storyline by selecting it and clicking "Activate"
- Generate player links via "🔗 Links" button
- Share player links with your players
- Click "+ New" in the control bar
- Enter a name for your storyline
- Click "Create"
Injects are the individual story beats shown to players.
- Click the "+ Add Inject" card in the storyline
- Fill in the details:
- Heading: Short title (shown prominently to players)
- Text: Main content/description
- GM Notes: Private notes only you can see
- Duration: Auto-advance time in seconds (0 = manual only)
- Day: Optional day number (0 = hidden, 1+ shows as "Day 1", "Day 2", etc.)
- Time: Optional time in HH:MM format (empty = hidden)
- Image: Optional image upload
- Target Player Types: Limit which players see this inject
- Click "Save Inject"
Day and Time are displayed to players above the inject heading in orange text, useful for showing story progression (e.g., "Day 1 — 09:00").
Branches allow side quests or alternate paths that diverge from the main storyline.
- Click the ⑂ branch button on any inject
- Configure the branch:
- Name: Descriptive name for the branch
- Auto-trigger: Automatically starts when parent inject is reached
- Manual trigger: GM must activate it manually
- Merge target: Where to resume the main storyline after branch completes
- Add injects to the branch
Branch Rules:
- An inject can have ONE auto-trigger branch OR multiple manual branches
- If an inject has an auto-trigger branch, no additional branches can be added
- Multiple manual branches on the same inject allow GM choice during play
Target specific content to specific players.
- Click "👥 Types" to manage player types
- Add types like "Wizard", "Knight", "Ranger"
- When creating injects, select which types should see it
- Players using type-specific links only see relevant content
- The "All Players" link sees everything
- Silent targeting: Players see no refresh or indication when an inject not meant for them is played
- Click "🔗 Links" to open the links panel
- Click "Generate Links" to create URLs
- Share type-specific links with players, or use the "All Players" link
- Regenerate creates new URLs (old links stop working)
The Notes for Later panel lets you capture observations during play.
- Type a note in the text area
- Press Enter to submit (or Alt+Enter for line breaks)
- Notes are saved with:
- Timestamp - When the note was created
- Inject reference - Which inject was active (e.g., "#4-2 Secret Cave" for branch inject 2 on main inject 4)
- Export notes to a text file for post-session review
- Clear All to reset notes for a new session
Session notes are stored separately from storyline data and persist across browser refreshes.
Two digital clocks are displayed next to the navigation buttons:
- Current Time - Shows the current time (HH:MM:SS)
- Session Stopwatch - Starts when you first advance the storyline, resets when you click Reset
The stopwatch helps track session duration without needing an external timer.
Hover over any inject card (in the storyline, branches, or library) to see a preview popup showing:
- Day/Time - If set for the inject
- Heading - The inject title
- Image - Thumbnail if an image is attached
- Text - The player-visible content
- GM Notes - Your private notes (shown in orange)
The preview appears after 700ms of hovering and follows your cursor.
- Use "◀ Prev" and "Next ▶" buttons to navigate
- Click inject numbers to jump directly (works for both main storyline and branch injects)
- Use keyboard shortcuts: ← → or Page Up/Down
- Click the ▶ Play button to start auto-playback
- Injects with durations will auto-advance
- Injects with 0 duration pause until you click Next
- Click ⏸ to pause
- Auto-trigger branches activate automatically when you reach their parent inject
- Manual branches must be activated by clicking "▶ Start" on the branch
- Active branches show with highlighted borders
- Click "⏹ Stop" to deactivate a branch - the current inject stays visible to players
- When you click "Next" after stopping a branch, playback returns to the main storyline
- When a branch completes naturally, playback returns to the main storyline (or merge target)
- ▶ Start: Activate the branch (players won't see any change until you advance)
- ⏹ Stop: Deactivate the branch (players continue seeing current inject until you advance)
- Activating/deactivating branches does NOT refresh the player view - only advancing does
The storyline view supports zooming for better overview:
- + / - buttons or keyboard shortcuts to zoom in/out
- ⊙ button or 0 key to reset to 100%
- ⊡ button or F key to auto-fit all injects in view
- When loading a new storyline, zoom automatically adjusts to fit all injects
- Scrollbars appear only when content overflows the visible area
- Activate the storyline you want to export
- Click "📤 Export"
- A JSON file will be downloaded containing the storyline and player types
- Click "📥 Import"
- Select a JSON file
- Preview shows what will be imported
- Click "Import" to add to your collection
Storylines use this structure:
{
"storylines": {
"uuid-here": {
"name": "Story Name",
"blocks": [
{
"id": "uuid",
"heading": "Title",
"text": "Content",
"gm_notes": "Private notes",
"duration": 30,
"day": 1,
"time": "09:00",
"target_player_types": ["Wizard"],
"image": null
}
],
"branches": [...]
}
},
"player_types": ["Wizard", "Knight", "Ranger"]
}See prompt.txt for a ChatGPT prompt that generates complete storylines in this format.
| Key | Action |
|---|---|
| ← / Page Up | Previous inject |
| → / Page Down | Next inject |
| A | Add new inject |
| T | Toggle theme |
| + / = | Zoom in |
| - | Zoom out |
| 0 | Reset zoom to 100% |
| F | Zoom to fit all injects |
Save frequently-used injects for reuse across storylines. The library panel is collapsible - click the header to expand or collapse it. The item count is always shown in the header.
- When editing an inject, check "📚 Save copy to Library"
- For branches, use "📚 Save to Library" button in branch editor
- Drag library injects onto the main storyline to add at specific positions
- Drag library injects onto a branch to add to that branch
- Drag library branches onto a main storyline inject to attach
- Click ➕ to add at the end of the storyline
storyteller/
├── app.py # Main entry point, Flask app setup
├── config.py # Configuration constants
├── data.py # Data persistence
├── routes.py # HTTP API endpoints
├── socket_handlers.py # WebSocket event handlers
├── prompt.txt # ChatGPT prompt for generating storylines
├── templates/
│ ├── gm.html # Game Master interface
│ ├── player.html # Player interface
│ └── login.html # GM login page
├── static/
│ ├── css/
│ │ ├── gm.css # GM styles
│ │ └── player.css # Player styles
│ └── js/
│ ├── gm.js # GM client logic
│ └── player.js # Player client logic
└── storyline_data/ # Runtime data (created automatically)
├── storylines.json # All storyline data (including images as base64)
└── session_notes.json # Session notes data
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/storylines |
List all storylines |
| POST | /api/storylines |
Create storyline |
| POST | /api/storylines/import |
Import from JSON |
| POST | /api/storylines/activate |
Set active storyline |
| GET | /api/storylines/<id> |
Get storyline |
| PUT | /api/storylines/<id> |
Update storyline |
| DELETE | /api/storylines/<id> |
Delete storyline |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/blocks |
Create inject |
| GET | /api/blocks/<storyline>/<id> |
Get inject |
| POST | /api/blocks/<storyline>/<id> |
Update inject |
| DELETE | /api/blocks/<storyline>/<id> |
Delete inject |
| POST | /api/blocks/reorder |
Reorder injects |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/branches |
Create branch |
| GET | /api/branches/<storyline>/<id> |
Get branch |
| PUT | /api/branches/<storyline>/<id> |
Update branch |
| DELETE | /api/branches/<storyline>/<id> |
Delete branch |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/player-types |
List player types |
| POST | /api/player-types |
Add player type |
| DELETE | /api/player-types/<n> |
Remove player type |
| GET | /api/player-links |
Get player links |
| POST | /api/player-links |
Generate/regenerate links |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/library |
List library items |
| POST | /api/library |
Add to library |
| POST | /api/library/<id>/add-to-storyline |
Add library inject to storyline |
| POST | /api/library/<id>/add-to-branch |
Add library inject to branch |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/session-notes |
Get all session notes |
| POST | /api/session-notes |
Add a session note |
| DELETE | /api/session-notes/<index> |
Delete a note by index |
| POST | /api/session-notes/clear |
Clear all session notes |
| Event | Data | Description |
|---|---|---|
gm_connected |
- | GM joins session |
player_connected |
{player_type_id} |
Player joins session |
next_block |
- | Advance to next inject |
previous_block |
- | Go to previous inject |
go_to_block |
{index} |
Jump to specific main inject |
go_to_branch_inject |
{branch_id, inject_index} |
Jump to specific branch inject |
toggle_playback |
{playing} |
Start/stop auto-play |
activate_branch |
{branch_id} |
Activate a branch |
deactivate_branch |
{branch_id} |
Deactivate a branch |
| Event | Data | Description |
|---|---|---|
block_update |
{block, current_index, ...} |
Current inject data |
state_update |
{current_block, ...} |
Playback state |
playback_update |
{playing, remaining} |
Timer state |
- Python 3.8+
- Flask
- Flask-SocketIO
- Werkzeug
- Eventlet
Dependencies are auto-installed on first run.
- Prepare in advance: Build your storyline before the session
- Use GM notes: Add reminders, questions to ask, or dice roll instructions
- Set strategic durations: Use auto-advance for atmospheric moments, manual for decision points
- Create branches for flexibility: Have side content ready if players go off-script
- Use manual branches for choices: Create multiple manual branches on a decision point inject to give players options
- Use merge targets strategically: Skip ahead in the main storyline after a branch to avoid repetition
- Use player types wisely: Give each player type their moment to shine
- Test player links: Verify links work before the session
- Keep a library: Save reusable elements like tavern descriptions or combat encounters
- Branch controls are invisible to players: Starting/stopping branches won't alert players - only advancing does
- Use Day/Time for pacing: Show story progression with day numbers and timestamps at key moments
- Generate with AI: Use the included
prompt.txtwith ChatGPT to generate complete storylines - Take session notes: Use the Notes for Later panel to capture observations for post-session review
- Export notes after sessions: Download your session notes before clearing for the next session
- Use zoom to fit: Press F to auto-zoom so all injects are visible at once
- Hover to preview: Hover over any inject to quickly see its content without opening the editor
- Track session time: The stopwatch starts automatically when you advance - use it to pace your session
MIT License - Feel free to use, modify, and distribute.