Last active
November 4, 2024 18:40
-
-
Save tobwen/9c5d109e7bdfce163e4315a10c199f5d to your computer and use it in GitHub Desktop.
human typing simulator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This code is under license CC0. | |
import argparse | |
import random | |
import sys | |
import time | |
from typing import List, Tuple | |
import datetime | |
# Keyboard layout mapping for common typos (QWERTY layout) | |
ADJACENT_KEYS = { | |
'a': ['q', 'w', 's', 'z'], | |
'b': ['v', 'n', 'h', 'g'], | |
'c': ['x', 'v', 'f', 'd'], | |
'd': ['s', 'f', 'g', 'e', 'r'], | |
'e': ['w', 'r', 'd', 's'], | |
'f': ['d', 'g', 'v', 'c', 'r', 't'], | |
'g': ['f', 'h', 'b', 'v', 't', 'y'], | |
'h': ['g', 'j', 'n', 'b', 'y', 'u'], | |
'i': ['u', 'o', 'k', 'j'], | |
'j': ['h', 'k', 'm', 'n', 'u', 'i'], | |
'k': ['j', 'l', 'm', 'i', 'o'], | |
'l': ['k', 'o', 'p'], | |
'm': ['n', 'j', 'k'], | |
'n': ['b', 'h', 'j', 'm'], | |
'o': ['i', 'p', 'l', 'k'], | |
'p': ['o', 'l'], | |
'q': ['w', 'a'], | |
'r': ['e', 't', 'f', 'd'], | |
's': ['a', 'd', 'x', 'w'], | |
't': ['r', 'y', 'g', 'f'], | |
'u': ['y', 'i', 'j', 'h'], | |
'v': ['c', 'b', 'f', 'g'], | |
'w': ['q', 'e', 's', 'a'], | |
'x': ['z', 'c', 's', 'd'], | |
'y': ['t', 'u', 'h', 'g'], | |
'z': ['a', 's', 'x'], | |
' ': ['b', 'n', 'm'] | |
} | |
class HumanTypingSimulator: | |
def __init__(self, | |
typo_probability: float = 0.05, | |
correction_probability: float = 0.9, | |
word_dropout_rate: float = 0.1, | |
delay_range: Tuple[float, float] = (0.05, 0.3)): | |
self.typo_probability = typo_probability | |
self.correction_probability = correction_probability | |
self.word_dropout_rate = word_dropout_rate | |
self.delay_min, self.delay_max = delay_range | |
def get_typing_delay(self, char: str) -> float: | |
"""Generate a human-like typing delay with context-based variability""" | |
random.seed() | |
base_delay = random.gauss( | |
(self.delay_min + self.delay_max) / 2, | |
(self.delay_max - self.delay_min) / 6 | |
) | |
jitter = random.uniform(-0.02, 0.02) | |
if char.isspace(): | |
pause = random.uniform(0.3, 0.6) | |
elif char in ",.!?": | |
pause = random.uniform(0.2, 0.4) | |
else: | |
pause = 0 | |
final_delay = base_delay + jitter + pause | |
return max(self.delay_min, min(final_delay, self.delay_max)) | |
def get_typo(self, char: str) -> str: | |
"""Get a possible typo for the given character""" | |
random.seed() | |
char = char.lower() | |
if char in ADJACENT_KEYS: | |
return random.choice(ADJACENT_KEYS[char]) | |
return char | |
def simulate_typing(self, text: str) -> None: | |
"""Simulate human typing with varied delays, contextual pauses, and word dropouts""" | |
words = text.split() | |
if not words: | |
return | |
# Determine if and which word to drop | |
dropped_word = None | |
dropped_index = -1 | |
if len(words) > 1 and random.random() < self.word_dropout_rate: | |
dropped_index = random.randint(0, len(words) - 1) | |
dropped_word = words.pop(dropped_index) | |
# Join the remaining words | |
current_text = ' '.join(words) | |
# Pre-calculate typos for the text | |
make_typos = [random.random() < self.typo_probability for _ in current_text] | |
typed_chars = 0 | |
buffer: List[str] = [] | |
# Type the main text | |
for i, char in enumerate(current_text): | |
time.sleep(self.get_typing_delay(char)) | |
if make_typos[i]: | |
typo = self.get_typo(char) | |
buffer.append(typo) | |
sys.stdout.write(typo) | |
sys.stdout.flush() | |
typed_chars += 1 | |
# Always correct typos | |
time.sleep(self.get_typing_delay(char)) | |
sys.stdout.write('\b \b') | |
sys.stdout.flush() | |
buffer.pop() | |
typed_chars -= 1 | |
time.sleep(self.get_typing_delay(char)) | |
buffer.append(char) | |
sys.stdout.write(char) | |
sys.stdout.flush() | |
typed_chars += 1 | |
else: | |
buffer.append(char) | |
sys.stdout.write(char) | |
sys.stdout.flush() | |
typed_chars += 1 | |
# If we dropped a word, go back and insert it | |
if dropped_word: | |
# Simulate realizing the mistake | |
time.sleep(random.uniform(0.8, 1.5)) | |
# Calculate position to insert the word | |
if dropped_index == 0: | |
insert_position = 0 | |
else: | |
insert_position = sum(len(words[i]) + 1 for i in range(dropped_index)) | |
# Move cursor back to insertion point | |
for _ in range(typed_chars - insert_position): | |
sys.stdout.write('\b') | |
sys.stdout.flush() | |
# Type the dropped word and a space | |
for char in dropped_word + ' ': | |
time.sleep(self.get_typing_delay(char)) | |
sys.stdout.write(char) | |
sys.stdout.flush() | |
# Retype the rest of the text | |
rest_of_text = current_text[insert_position:] | |
for char in rest_of_text: | |
time.sleep(self.get_typing_delay(char)) | |
sys.stdout.write(char) | |
sys.stdout.flush() | |
def parse_delay_range(delay_range_str: str) -> Tuple[float, float]: | |
"""Parse delay range string in format 'min-max'""" | |
try: | |
min_delay, max_delay = map(float, delay_range_str.split('-')) | |
if min_delay < 0 or max_delay < 0 or min_delay >= max_delay: | |
raise ValueError | |
return (min_delay, max_delay) | |
except ValueError: | |
raise argparse.ArgumentTypeError( | |
"Delay range must be in format 'min-max' where min and max are positive numbers and min < max" | |
) | |
def main(): | |
parser = argparse.ArgumentParser(description='Simulate human-like typing with typos and delays') | |
parser.add_argument('text', help='Text to type') | |
parser.add_argument('--typo-probability', type=float, default=0.05, | |
help='Probability of making a typo (default: 0.05)') | |
parser.add_argument('--correction-probability', type=float, default=0.9, | |
help='Probability of correcting a typo (default: 0.9)') | |
parser.add_argument('--word-dropout-rate', type=float, default=0.1, | |
help='Probability of dropping a word and adding it later (default: 0.1)') | |
parser.add_argument('--delay-range', type=parse_delay_range, default='0.05-0.3', | |
help='Delay range in seconds (format: min-max, default: 0.05-0.3)') | |
args = parser.parse_args() | |
simulator = HumanTypingSimulator( | |
typo_probability=args.typo_probability, | |
correction_probability=args.correction_probability, | |
word_dropout_rate=args.word_dropout_rate, | |
delay_range=args.delay_range | |
) | |
simulator.simulate_typing(args.text) | |
sys.stdout.write('\n') | |
if __name__ == '__main__': | |
main() | |
""" | |
Example usages: | |
# Default usage | |
python3 typing_simulator.py "Hello, World!" | |
# Fast typing (50-150ms between keystrokes) | |
python3 typing_simulator.py --delay-range 0.05-0.15 "Hello, World!" | |
# Very fast typing (20-80ms between keystrokes) | |
python3 typing_simulator.py --delay-range 0.02-0.08 "Hello, World!" | |
# Slow typing (200-500ms between keystrokes) | |
python3 typing_simulator.py --delay-range 0.2-0.5 "Hello, World!" | |
# Higher word dropout rate (30% chance to drop a word) | |
python3 typing_simulator.py --word-dropout-rate 0.3 "The quick brown fox jumps over the lazy dog" | |
# Fast typing with high typo rate and word dropouts | |
python3 typing_simulator.py --delay-range 0.02-0.05 --typo-probability 0.2 --word-dropout-rate 0.9 "The quick brown fox jumps over the lazy dog" | |
""" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment