#!/usr/bin/env python3
"""
Keyboard Analytics - A tool for analyzing keyboard usage patterns
This script analyzes keyboard events stored in a SQLite database and generates
visualizations and reports based on the data. It can track key presses across
different time periods and create meaningful insights about typing patterns.
"""
import sqlite3
import time
import pathlib
import json
import logging
import requests
from typing import List, Dict, Tuple, Any, Set
import matplotlib.pyplot as plt
from xmlrpc.client import ServerProxy
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Initialize API connection
API_ENDPOINT = "https://api.molodetz.nl/rpc"
api = ServerProxy(API_ENDPOINT)
# Database connection
DB_PATH = 'tikker.db'
connection = sqlite3.connect(DB_PATH)
# Track processed items to avoid duplicate work
processed_files: Set[str] = set()
processed_weeks: Set[str] = set()
# SQL helper for weekday names
WEEKDAY_SQL = """
CASE
WHEN strftime('%w', timestamp) = '0' THEN 'Sunday'
WHEN strftime('%w', timestamp) = '1' THEN 'Monday'
WHEN strftime('%w', timestamp) = '2' THEN 'Tuesday'
WHEN strftime('%w', timestamp) = '3' THEN 'Wednesday'
WHEN strftime('%w', timestamp) = '4' THEN 'Thursday'
WHEN strftime('%w', timestamp) = '5' THEN 'Friday'
WHEN strftime('%w', timestamp) = '6' THEN 'Saturday'
END
"""
def query(sql: str) -> List[Tuple]:
"""
Execute an SQL query and return the results.
Args:
sql: SQL query to execute
Returns:
List of result tuples
"""
start = time.time()
cursor = connection.cursor()
logger.debug(f"Executing SQL: {sql}")
result = cursor.execute(sql).fetchall()
cursor.close()
duration = time.time() - start
logger.info(f"Query completed in {duration:.4f} seconds")
return result
def setup_plot_defaults() -> None:
"""Configure default matplotlib settings for all plots"""
plt.style.use('dark_background')
plt.figure(figsize=(10, 7))
def save_figure(filename: str) -> None:
"""Save the current matplotlib figure to a file"""
plt.tight_layout()
plt.savefig(filename)
plt.close()
logger.info(f"Figure saved: {filename}")
def render_per_hour(week: str) -> None:
"""
Generate visualization of key presses per hour for a specific week
Args:
week: Week number to analyze
"""
# Skip if already processed
if week in processed_weeks:
logger.info(f"Week {week} already processed for hourly analysis, skipping")
return
processed_weeks.add(week)
sql_presses = """
SELECT count(0) as total, strftime('%H', timestamp) as hour,
strftime('%U', timestamp) as week
FROM kevent
WHERE event = 'PRESSED' AND week = '{week}'
GROUP BY week, hour
ORDER BY hour
"""
rows_presses = query(sql_presses.format(week=week))
if not rows_presses:
logger.warning(f"No data found for week {week}")
return
totals = [row[0] for row in rows_presses]
hours = [row[1] for row in rows_presses]
setup_plot_defaults()
plt.plot(hours, totals, marker='o', label=f'Presses per hour', color='red')
plt.xlabel('Hour')
plt.ylabel('Event count')
plt.title(f'Key presses per hour - Week {week}')
plt.legend()
save_figure(f"graph_week_{week.strip('\'')}_per_hour.png")
def render_per_day() -> None:
"""Generate visualization of key presses per day"""
sql_pressed_per_day = """
SELECT strftime('%Y-%m-%d', timestamp) as month_day,
count(0) as total
FROM kevent
WHERE event = 'PRESSED'
GROUP BY month_day
ORDER BY month_day
"""
rows_pressed_per_day = query(sql_pressed_per_day)
if not rows_pressed_per_day:
logger.warning("No data found for daily analysis")
return
dates = [row[0] for row in rows_pressed_per_day]
totals = [row[1] for row in rows_pressed_per_day]
setup_plot_defaults()
plt.plot(dates, totals, marker='o', label='Presses per day', color='red')
plt.xlabel('Date')
plt.ylabel('Event count')
plt.xticks(rotation=45)
plt.title('Keyboard events by day')
plt.legend()
save_figure("graph_per_day.png")
def render_per_week() -> None:
"""Generate visualization of key presses per week"""
sql_pressed_per_week = """
SELECT strftime('%Y-%U', timestamp) as week,
count(0) as total
FROM kevent
WHERE event = 'PRESSED'
GROUP BY week
ORDER BY week
"""
rows_pressed_per_week = query(sql_pressed_per_week)
if not rows_pressed_per_week:
logger.warning("No data found for weekly analysis")
return
weeks = [row[0] for row in rows_pressed_per_week]
totals = [row[1] for row in rows_pressed_per_week]
setup_plot_defaults()
plt.plot(weeks, totals, marker='o', label='Presses per week', color='red')
plt.xlabel('Week')
plt.ylabel('Presses count')
plt.xticks(rotation=45)
plt.title('Presses per week')
plt.legend()
save_figure("graph_per_week.png")
def render_per_weekday(week: str) -> None:
"""
Generate visualization of key presses per weekday for a specific week
Args:
week: Week number to analyze
"""
# Skip if already processed
if week in processed_weeks:
logger.info(f"Week {week} already processed for weekday analysis, skipping")
return
processed_weeks.add(week)
sql_presses = f"""
SELECT count(0) as total, {WEEKDAY_SQL} as weekday,
strftime('%w', timestamp) as day, strftime('%U', timestamp) as week
FROM kevent
WHERE event = 'PRESSED' AND week = '{week}'
GROUP BY week, day
ORDER BY day
"""
rows_presses = query(sql_presses)
if not rows_presses:
logger.warning(f"No data found for week {week} weekday analysis")
return
totals = [row[0] for row in rows_presses]
days = [row[2] for row in rows_presses]
weekday_names = [row[1] for row in rows_presses]
setup_plot_defaults()
plt.plot(days, totals, marker='o', label='Press count', color='red')
plt.xlabel('Weekday')
plt.ylabel('Event count')
plt.title(f'Presses per weekday - Week {week}')
plt.xticks(range(len(weekday_names)), weekday_names, rotation=45)
plt.legend()
save_figure(f"graph_week_{week.strip('\"')}_per_weekday.png")
def get_weeks() -> List[str]:
"""
Get list of all weeks in the database
Returns:
List of week numbers
"""
sql = "SELECT DISTINCT strftime('%U', timestamp) as week FROM kevent GROUP BY week"
weeks = query(sql)
return [record[0] for record in weeks]
def get_score_per_week() -> List[Tuple]:
"""
Get event counts grouped by week
Returns:
List of (week, event_type, count) tuples
"""
sql = """
SELECT strftime('%U', timestamp) as week, event, COUNT(0) as total
FROM kevent
GROUP BY event, week
"""
return query(sql)
def get_score_per_day() -> List[Tuple]:
"""
Get event counts grouped by day of week
Returns:
List of (week, weekday, event_type, count) tuples
"""
sql = f"""
SELECT strftime('%U', timestamp) as week,
{WEEKDAY_SQL} as wday,
event, COUNT(0) as total
FROM kevent
WHERE event in ('PRESSED')
GROUP BY week, event, wday
ORDER BY week, event, wday
"""
return query(sql)
def get_totals() -> List[Tuple]:
"""
Get total count of each event type
Returns:
List of (count, event_type) tuples
"""
sql = "SELECT count(0) as total, event FROM kevent GROUP BY event"
return query(sql)
def generate_keylog() -> Dict[str, str]:
"""
Generate a log of key presses grouped by date and hour
Returns:
Dictionary of date-hour to concatenated key presses
"""
result = {}
rows = query("""
SELECT strftime('%Y-%m-%d.%H', timestamp) as date_hour,
GROUP_CONCAT(char,'')
FROM kevent
WHERE event = 'PRESSED'
GROUP BY date_hour
""")
for row in rows:
result[row[0]] = row[1]
return result
def write_keylog_files(keylog: Dict[str, str]) -> None:
"""
Write keylog data to files
Args:
keylog: Dictionary of date-hour to concatenated key presses
"""
logs_dir = pathlib.Path("logs_plain")
logs_dir.mkdir(exist_ok=True)
with open("keylog.txt", "w") as f:
for day in keylog.keys():
date, hour = day.split(".")
label = f"{date} {hour}:00"
log_file = logs_dir / f"{day}.txt"
if not log_file.exists():
with open(log_file, "w") as g:
g.write(f"**{label}**: ```{keylog[day]}```\n\n")
f.write(f"**{label}**: ```{keylog[day]}```\n\n")
def ipa(prompt):
import requests
result = requests.post("https://retoor:retoorded@ipa.molodetz.nl/ai/prompt",json={"prompt": prompt, "model": "google/gemma-3-12b-it","json":False}).text
print(result)
return result
def generate_summaries(dry_run: bool=False) -> None:
"""Generate summaries for keylog files using AI API"""
logs_dir = pathlib.Path("logs_plain")
summary_dir = pathlib.Path("logs_summaries")
oneliner_dir = pathlib.Path("logs_lines")
summary_dir.mkdir(exist_ok=True)
oneliner_dir.mkdir(exist_ok=True)
# Process summaries
for file in logs_dir.glob("*.txt"):
# Skip if already processed
if str(file) in processed_files:
logger.info(f"File {file} already processed for summary, skipping")
continue
processed_files.add(str(file))
dest_file = summary_dir / file.name
if dest_file.exists():
logger.info(f"Summary already exists for {file.name}, skipping")
continue
try:
logger.info(f"Generating summary for {file.name}")
if dry_run:
continue
param = file.read_text().replace("@", "").replace("`", "")
prompt = "The following data is key presses made by user. Describe what user could be working on using bulletpoints: " + param
response = ipa(prompt)
with dest_file.open("w+") as f:
f.write(response)
logger.info(f"Summary generated for {file.name}")
except Exception as e:
logger.error(f"Error generating summary for {file.name}: {e}")
# Process one-liners
for file in summary_dir.glob("*.txt"):
# Skip if already processed
if str(file) in processed_files:
logger.info(f"File {file} already processed for one-liner, skipping")
continue
processed_files.add(str(file))
dest_file = oneliner_dir / file.name
if dest_file.exists():
logger.info(f"One-liner already exists for {file.name}, skipping")
continue
try:
logger.info(f"Generating one-liner for {file.name}")
if dry_run:
continue
source = file.read_text().replace("@", "").replace("`", "")
prompt = "The following data is a hour of work summarized from the user. Describe what user was doing in a oneliner: " + source
response = ipa(prompt)
with dest_file.open("w+") as f:
f.write(response)
logger.info(f"One-liner generated for {file.name}")
except Exception as e:
logger.error(f"Error generating one-liner for {file.name}: {e}")
def main() -> None:
# Generate summaries
generate_summaries(False)
"""Main function to execute all analytics tasks"""
time_start = time.time()
logger.info("Starting keyboard analytics process")
# Load state if exists
state_file = pathlib.Path("analytics_state.json")
if state_file.exists():
try:
state = json.loads(state_file.read_text())
processed_files.update(state.get("processed_files", []))
processed_weeks.update(state.get("processed_weeks", []))
logger.info(f"Loaded state: {len(processed_files)} files and {len(processed_weeks)} weeks processed previously")
except Exception as e:
logger.error(f"Error loading state: {e}")
# Generate visualizations
render_per_day()
render_per_week()
weeks = get_weeks()
for week in weeks:
render_per_hour(week)
render_per_weekday(week)
# Print statistics
logger.info("Score per week:")
for record in get_score_per_week():
logger.info(f"{record[0]}\t{record[1]}\t{record[2]}")
logger.info("Score per day:")
for record in get_score_per_day():
logger.info(f"{record[0]}\t{record[1]}\t{record[2]}\t{record[3]}")
logger.info("Total events:")
totals = 0
for record in get_totals():
logger.info(f"{record[1]}: {record[0]}")
totals += record[0]
logger.info(f"Total: {totals}")
# Generate and write keylog
keylog = generate_keylog()
write_keylog_files(keylog)
# Generate summaries
generate_summaries()
# Save state
try:
state = {
"processed_files": list(processed_files),
"processed_weeks": list(processed_weeks),
"last_run": time.time()
}
state_file.write_text(json.dumps(state))
logger.info("State saved successfully")
except Exception as e:
logger.error(f"Error saving state: {e}")
duration = time.time() - time_start
logger.info(f"Process completed in {duration:.2f} seconds")
if __name__ == "__main__":
main()