Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions images/chromium-headful/demos/demo_smooth_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
Demo script: smooth typing vs instant typing.

Drives the typing_demo.html page through the kernel-images API to produce
a side-by-side comparison suitable for recording as a GIF/MP4.

Usage:
# 1. Start a kernel-images container
# 2. Upload typing_demo.html to the container
# 3. Run this script:
python demo_smooth_typing.py --base-url http://localhost:8000

Requirements:
pip install requests
"""

import argparse
import base64
import json
import time
from pathlib import Path

import requests

DEMO_TEXT = "The quick brown fox jumps over the lazy dog. Hello world!"


def api(base: str, method: str, path: str, **kwargs):
url = f"{base}{path}"
resp = getattr(requests, method)(url, **kwargs)
resp.raise_for_status()
return resp


def upload_demo_page(base: str):
html_path = Path(__file__).parent / "typing_demo.html"
html_bytes = html_path.read_bytes()
api(base, "put", "/fs/write_file", params={"path": "/tmp/typing_demo.html"},
data=html_bytes, headers={"Content-Type": "application/octet-stream"})
print("Uploaded typing_demo.html")


def execute_js(base: str, code: str):
api(base, "post", "/playwright/execute", json={"code": code, "timeout_sec": 10})


def navigate(base: str):
execute_js(base, "await page.goto('file:///tmp/typing_demo.html');")
time.sleep(1)


def click_input(base: str):
execute_js(base, "await page.click('#input');")
time.sleep(0.3)


def clear_input(base: str):
execute_js(base, "window.demoApi.clear();")
time.sleep(0.3)


def set_mode(base: str, label: str, cls: str):
execute_js(base, f"window.demoApi.setMode('{label}', '{cls}');")


def type_text(base: str, text: str, smooth: bool = False, typo_chance: float = 0):
body = {"text": text, "smooth": smooth}
if typo_chance > 0:
body["typo_chance"] = typo_chance
if not smooth:
body["delay"] = 0
api(base, "post", "/computer/type", json=body)


def start_recording(base: str):
api(base, "post", "/recording/start", json={"framerate": 15, "id": "typing-demo"})
print("Recording started")
time.sleep(0.5)


def stop_recording(base: str):
api(base, "post", "/recording/stop", json={"id": "typing-demo"})
time.sleep(1)
print("Recording stopped")


def download_recording(base: str, output: str):
resp = api(base, "get", "/recording/download", params={"id": "typing-demo"})
Path(output).write_bytes(resp.content)
print(f"Saved recording to {output}")


def run_demo(base: str, output: str):
upload_demo_page(base)
navigate(base)
start_recording(base)

# --- Phase 1: Instant typing (no delay) ---
set_mode(base, "INSTANT TYPING — delay: 0", "instant")
time.sleep(1)
click_input(base)
type_text(base, DEMO_TEXT, smooth=False)
time.sleep(2)

# --- Phase 2: Smooth typing (no typos) ---
clear_input(base)
set_mode(base, "SMOOTH TYPING — HUMAN-LIKE", "smooth")
time.sleep(1)
click_input(base)
type_text(base, DEMO_TEXT, smooth=True)
time.sleep(2)

# --- Phase 3: Smooth typing with typos ---
clear_input(base)
set_mode(base, "SMOOTH TYPING — WITH TYPOS", "typos")
time.sleep(1)
click_input(base)
type_text(base, DEMO_TEXT, smooth=True, typo_chance=0.04)
time.sleep(2)

stop_recording(base)
download_recording(base, output)


def main():
parser = argparse.ArgumentParser(description="Smooth typing demo recorder")
parser.add_argument("--base-url", default="http://localhost:8000",
help="Base URL of the kernel-images API")
parser.add_argument("--output", default="smooth_typing_demo.mp4",
help="Output video file path")
args = parser.parse_args()
run_demo(args.base_url, args.output)


if __name__ == "__main__":
main()
170 changes: 170 additions & 0 deletions images/chromium-headful/demos/typing_demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Typing Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; }
body {
font-family: 'SF Mono', 'Fira Code', 'Monaco', monospace;
background: #0d1117;
color: #e6edf3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 48px;
}
#banner {
padding: 14px 40px;
background: rgba(22, 27, 34, 0.95);
border: 2px solid #30363d;
border-radius: 12px;
font-size: 22px;
font-weight: 700;
letter-spacing: 6px;
text-transform: uppercase;
color: #58a6ff;
text-align: center;
transition: color 0.3s, border-color 0.3s;
}
#banner.instant { color: #f85149; border-color: #f85149; }
#banner.smooth { color: #3fb950; border-color: #3fb950; }
#banner.typos { color: #d2a8ff; border-color: #d2a8ff; }
.typing-area {
width: 700px;
min-height: 120px;
padding: 24px 28px;
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
position: relative;
}
.typing-area .label {
position: absolute;
top: -12px;
left: 16px;
background: #0d1117;
padding: 2px 10px;
font-size: 11px;
color: #8b949e;
letter-spacing: 2px;
text-transform: uppercase;
}
#input {
width: 100%;
background: transparent;
border: none;
outline: none;
color: #e6edf3;
font-family: inherit;
font-size: 20px;
line-height: 1.6;
caret-color: #58a6ff;
resize: none;
overflow: hidden;
}
#input::placeholder { color: #484f58; }
#keystroke-viz {
width: 700px;
height: 80px;
position: relative;
overflow: hidden;
}
#keystroke-viz canvas {
width: 100%;
height: 100%;
}
.hint {
color: #484f58;
font-size: 12px;
letter-spacing: 1px;
}
</style>
</head>
<body>
<div id="banner">Typing Demo</div>

<div class="typing-area">
<div class="label">Input</div>
<textarea id="input" rows="3" placeholder="Text will appear here..."></textarea>
</div>

<div id="keystroke-viz">
<canvas id="viz-canvas"></canvas>
</div>

<div class="hint">Keystroke timing visualization</div>

<script>
const input = document.getElementById('input');
const banner = document.getElementById('banner');
const vizCanvas = document.getElementById('viz-canvas');
const vizCtx = vizCanvas.getContext('2d');

let keyTimes = [];
let lastKeyTime = 0;

function resizeCanvas() {
vizCanvas.width = vizCanvas.parentElement.clientWidth;
vizCanvas.height = vizCanvas.parentElement.clientHeight;
drawViz();
}

function drawViz() {
const w = vizCanvas.width;
const h = vizCanvas.height;
vizCtx.clearRect(0, 0, w, h);

if (keyTimes.length < 2) return;

const intervals = [];
for (let i = 1; i < keyTimes.length; i++) {
intervals.push(keyTimes[i] - keyTimes[i - 1]);
}

const maxInterval = Math.min(Math.max(...intervals), 500);
const barWidth = Math.max(2, (w - 20) / intervals.length - 1);

vizCtx.fillStyle = 'rgba(88, 166, 255, 0.7)';
const bannerEl = document.getElementById('banner');
if (bannerEl.classList.contains('instant')) vizCtx.fillStyle = 'rgba(248, 81, 73, 0.7)';
if (bannerEl.classList.contains('smooth')) vizCtx.fillStyle = 'rgba(63, 185, 80, 0.7)';
if (bannerEl.classList.contains('typos')) vizCtx.fillStyle = 'rgba(210, 168, 255, 0.7)';

for (let i = 0; i < intervals.length; i++) {
const barH = Math.max(2, (intervals[i] / maxInterval) * (h - 10));
const x = 10 + i * (barWidth + 1);
vizCtx.fillRect(x, h - barH, barWidth, barH);
}
}

input.addEventListener('keydown', () => {
const now = performance.now();
keyTimes.push(now);
if (keyTimes.length > 300) keyTimes.shift();
drawViz();
});

window.addEventListener('resize', resizeCanvas);
resizeCanvas();

window.demoApi = {
setMode: (label, cls) => {
banner.textContent = label;
banner.className = cls || '';
},
clear: () => {
input.value = '';
keyTimes = [];
vizCtx.clearRect(0, 0, vizCanvas.width, vizCanvas.height);
},
focus: () => {
input.focus();
}
};
</script>
</body>
</html>
Loading
Loading