How to create, generate, and compose demo screencasts for Purple Computer.
- Quick Start
- Composing a Demo
- Generating Segments
- Fallback Chain
- How It Works
- Mode Behaviors (for Scripting)
- Action Reference
- Color Reference
- Troubleshooting
# Generate a Music Room segment
./tools/play-ai "smiley face" --save smiley
# Generate an Art segment
./tools/doodle-ai --goal "palm tree"
./tools/install-doodle-demo --from doodle_ai_output/TIMESTAMP --save palm_tree
# Run it
make run-demoEach --save creates a segment file in purple_tui/demo/segments/ and adds it to demo.json. The demo plays all segments in order.
The file purple_tui/demo/demo.json lists which segments to play and in what order:
[
{"segment": "smiley"},
{"segment": "palm_tree", "speed": 8.0}
]Each entry names a Python module in purple_tui/demo/segments/. The speed field is optional.
You don't need to create this file manually. --save builds it up as you generate segments. To start over, delete the file.
Just edit the JSON array:
[
{"segment": "palm_tree", "speed": 8.0},
{"segment": "smiley"}
]Delete its line from demo.json. You can also delete the segment file from purple_tui/demo/segments/ if you no longer need it.
Speed controls how fast a segment plays back. Higher = faster.
Three places can set it, checked in this order:
"speed"indemo.json(highest priority)SPEED_MULTIPLIERin the segment file- Default:
1.0
The player inserts a SetSpeed action before each segment, so different segments can run at different speeds.
Generates a Music Room composition (music + colored grid art) from a text prompt:
./tools/play-ai "smiley face" --save smiley
./tools/play-ai "heart" --save heart
./tools/play-ai "rainstorm" --save rainstorm --no-reviewOptions:
--save NAME: save as a named segment and add todemo.json--no-review: skip the AI review pass (faster, less accurate)--json: print raw JSON instead of Python code
Without --save, it just prints the generated code to stdout.
Two steps: generate the drawing, then install it as a segment.
Step 1: Generate the drawing
./tools/doodle-ai --goal "palm tree"This creates a timestamped folder like doodle_ai_output/20260202_143022/ with screenshots of each iteration.
Step 2: Review the results
Check the screenshots to pick your favorite:
doodle_ai_output/20260202_143022/screenshots/
iteration_0_blank.svg
iteration_0_blank_cropped.png
iteration_1.svg
iteration_1_cropped.png
iteration_2.svg
...
The _cropped.png files show exactly what the AI saw. Or trust the AI's judgment (stored in best_iteration.json).
Step 3: Install as a segment
# Use best iteration (default)
./tools/install-doodle-demo --from doodle_ai_output/20260202_143022 --save palm_tree
# From a specific screenshot
./tools/install-doodle-demo --from doodle_ai_output/20260202_143022/screenshots/iteration_2b_refinement_cropped.png --save palm_tree
# Pick iteration and duration
./tools/install-doodle-demo --from doodle_ai_output/20260202_143022 --iteration 3 --duration 15 --save palm_treeOptions:
--save NAME: save as a named segment and add todemo.json--iteration X: use a specific iteration instead of the best--duration N: target playback duration in seconds (default: 10)
Without --save, it writes to ai_generated_script.py (legacy behavior).
Create a Python file in purple_tui/demo/segments/:
# purple_tui/demo/segments/greeting.py
from ..script import SwitchMode, TypeText, PressKey, Pause
SEGMENT = [
SwitchMode("play"),
TypeText("hello!"),
PressKey("enter", pause_after=1.0),
Pause(1.5),
]Rules:
- Export a
SEGMENTlist ofDemoActionobjects - Each segment should include its own
SwitchModeat the start - Optionally export
SPEED_MULTIPLIER(float, default 1.0)
Then add it to demo.json manually or with:
# From tools/ directory
from ai_utils import append_to_demo_json
append_to_demo_json("greeting")The demo system checks these in order:
demo.jsonexists: load composed segmentsai_generated_script.pyexists: use the monolithic AI script (legacy)default_script.py: the hand-crafted default
Delete demo.json to fall back to option 2 or 3.
demo.json (composition: which segments, in what order)
|
v
segments/smiley.py (SEGMENT = [...], SPEED_MULTIPLIER = 1.0)
segments/palm_tree.py
|
v
get_demo_script() (loads demo.json, imports segments, inserts SetSpeed)
|
v
DemoPlayer.play() (dispatches actions as synthetic keyboard events)
|
v
handle_keyboard_action() (same path as real keyboard input)
Key files:
purple_tui/demo/script.py: action dataclasses (TypeText, PlayKeys, DrawPath, etc.)purple_tui/demo/player.py: executes scripts by dispatching actionspurple_tui/demo/__init__.py:get_demo_script()and composition loadingpurple_tui/demo/segments/: one file per segmentpurple_tui/demo/default_script.py: the hand-crafted fallback
Each segment file exports:
SEGMENT: list[DemoAction] # required
SPEED_MULTIPLIER: float = 1.0 # optionalThe composition loader wraps each segment with a SetSpeed action, so segments don't need to manage speed themselves.
@dataclass
class SetSpeed(DemoAction):
multiplier: float = 1.0Inserted automatically between segments during composition loading. Updates the player's speed multiplier on the fly. Higher values = faster playback.
DemoPlayer(
dispatch_action=app._dispatch_keyboard_action,
speed_multiplier=1.0,
clear_all=app.clear_all_state,
set_play_key_color=app._set_play_key_color,
is_doodle_paint_mode=app._is_doodle_paint_mode,
)clear_all: called byClearAll()to reset all modesset_play_key_color: direct color control for Music Room keysis_doodle_paint_mode: letsDrawPathcheck if Tab is needed
- 10x4 grid mapped to the keyboard
- Each key press cycles the color: off -> purple -> blue -> red -> off
- Colors persist until cycled again
- Every press plays a sound, so pressing a key 3x for red also plays 3 notes
The grid:
1 2 3 4 5 6 7 8 9 0 (percussion)
Q W E R T Y U I O P (high marimba)
A S D F G H J K L ; (mid marimba)
Z X C V B N M , . / (low marimba)
Strategy: plan which keys to press to form a picture. Each key should be pressed exactly the right number of times for its target color.
PlayKeys(sequence=['e', None, 'i'], seconds_between=0.67) # Eyes (purple, 1 press)
PlayKeys(sequence=['a', None, 'l'], seconds_between=0.6) # Corners (purple)
PlayKeys(sequence=['c', 'v', 'b', 'n'], seconds_between=0.43) # Smile (purple)Two sub-modes:
- Text mode (default): typing places letters on the canvas
- Paint mode (Tab to toggle): letter keys select brush color and stamp
Paint mode colors by keyboard row:
- QWERTY row: red family (light to dark, left to right)
- ASDF row: yellow family
- ZXCV row: blue family
- Number row: grayscale
Drawing mechanics:
- Lowercase key: select color AND stamp, then advance cursor
- Shift+key: select color only (no stamp)
- Space+arrows: draw lines (hold space while moving)
DrawPathhandles all of this automatically
Text input with results below. Just use TypeText + PressKey("enter").
SwitchMode("play"),
TypeText("red + blue"),
PressKey("enter", pause_after=1.5),TypeText("hello!", delay_per_char=0.08, final_pause=0.3)PressKey("enter", pause_after=0.5)
PressKey("tab")
PressKey("space", hold_duration=0.5) # hold then releaseKeys: enter, backspace, escape, tab, space, up, down, left, right
PlayKeys(sequence=['a', 's', ['d', 'f'], None], seconds_between=0.5, pause_after=0.5)'a': single key['d', 'f']: chord (simultaneous)None: rest
DrawPath(directions=['right', 'right', 'down'], color_key='f', delay_per_step=0.1)Automatically enters paint mode. Holds space while moving.
MoveSequence(directions=['right', 'right', 'down'], delay_per_step=0.01)Moves cursor without painting. For repositioning between draws.
SwitchMode("music") # F2
SwitchMode("play") # F1
SwitchMode("art") # F3Pause(1.5)ClearAll() # reset all modesSetSpeed(multiplier=2.0) # 2x speed from here onNormally inserted automatically by the composition loader.
| Presses | Color | Hex |
|---|---|---|
| 0 | Off | default |
| 1 | Purple | #da77f2 |
| 2 | Blue | #4dabf7 |
| 3 | Red | #ff6b6b |
| 4 | Off | cycles |
| Row | Keys | Colors |
|---|---|---|
| 1-0 | number row | Grayscale |
| QWERTY | q,w,e,r,t,y,u,i,o,p | Red family |
| ASDF | a,s,d,f,g,h,j,k,l | Yellow family |
| ZXCV | z,x,c,v,b,n,m | Blue family |
Within each row, colors go light (left) to dark (right).
Overlapping paint strokes mix subtractively:
- Red + Blue = Purple
- Red + Yellow = Orange
- Blue + Yellow = Green
Music room shows a blob instead of a shape. Plan your key sequence on the grid first. Mark which keys form the picture before writing code.
Music room colors are wrong. A key pressed N times lands on color (N % 4). Press once for purple, twice for blue, three times for red. Four presses cycle back to off.
Art room types letters instead of painting.
You're in text mode. Press Tab to switch to paint mode. DrawPath does this automatically, but manual PressKey sequences don't.
Art room colors are wrong. Check which keyboard row the key is on (QWERTY=red, ASDF=yellow, ZXCV=blue).
Demo doesn't use my segments.
Check that demo.json exists in purple_tui/demo/ and that the segment names match files in purple_tui/demo/segments/.
Demo plays the old monolithic script.
Delete demo.json to fall back to ai_generated_script.py, or delete both to fall back to default_script.py.
Tempo reference:
- 90 bpm = 0.67s per note
- 120 bpm = 0.5s per note
- 180 bpm = 0.33s per note