Build a Smart Clipboard: Summarize & Translate Anything You Copy with Ollama
What We're Building
We’re building a lightweight, local-first smart clipboard that watches for new text, then lets you hit a hotkey to transform it instantly. Out of the box, it summarizes, translates, rephrases, or explains whatever you’ve copied—running entirely on your machine with zero API costs.
Feature list:
- Background daemon that polls the system clipboard for changes.
- Sends clipboard text to a local LLM via Ollama (Llama 3.2 3B, or any model you have pulled).
- Configurable global hotkey (
Ctrl+Shift+Space) to trigger a pop-up. - Pop-up displays the LLM response and offers action buttons: Summarize, Translate (to English), Explain, Rephrase.
- Caches responses to avoid re-processing identical text.
- Runs entirely offline after model pull; no API keys, no telemetry.
If you’ve ever copied a dense paragraph and wished a little AI assistant would just tell you what it says, you’re in the right place. This pattern is the same one FDEs use to build quick internal tools that shave minutes off repetitive reading—think of it as your first step toward the kind of workflow automation we cover in Build an On-Call Incident Summarizer That Reads Logs and Drafts a Postmortem with Gemini.
Architecture Overview
Before we write a line of code, let’s map the data flow. We have three main processes: the clipboard watcher, the LLM client, and the pop-up UI.
The daemon uses pyperclip to grab clipboard content and a simple text hash to detect changes. When you press the hotkey, the latest text and your chosen action are sent to Ollama’s chat endpoint. The response renders in a Tkinter window. Caching means if you hit “Summarize” twice on the same paragraph, you get the cached result instantly.
Prerequisites & Free Tools
Everything here is free—no credit card, no trial expiration.
| Tool | Purpose | Install / Link |
|---|---|---|
| Python 3.10+ | Runtime | python.org/downloads |
| Ollama | Local LLM server | ollama.com/download |
| Llama 3.2 3B | Lightweight model | ollama pull llama3.2:3b |
| pynput | Global hotkey listener | pip install pynput |
| pyperclip | Clipboard access | pip install pyperclip |
| requests | HTTP calls to Ollama | pip install requests |
| Tkinter | GUI (bundled with Python on most systems) | Pre-installed; on Linux: sudo apt install python3-tk |
If you’re on macOS, pynput needs Accessibility permissions—grant them when prompted. On Linux, you may need xclip or xsel for pyperclip: sudo apt install xclip.
Step 1: Setting Up Ollama
Install Ollama from the link above, then pull the model:
ollama pull llama3.2:3b
Verify it’s running:
ollama run llama3.2:3b "Say 'clipboard ready' in one word."
You should see ready (or similar). The Ollama server listens on http://localhost:11434 by default. We’ll hit /api/chat with a JSON payload.
For a deeper dive into running models locally at scale, check out Petals: Running Large Language Models at Home with a BitTorrent-Style Network. But for our single-user tool, one Ollama instance is plenty.
Step 2: The Clipboard Watcher Daemon
Create clipboard_daemon.py. This module handles clipboard polling and change detection.
import pyperclip
import hashlib
import time
import threading
class ClipboardWatcher:
def __init__(self, poll_interval=0.5):
self.poll_interval = poll_interval
self.current_text = ""
self.current_hash = ""
self.lock = threading.Lock()
self._running = False
def _hash(self, text: str) -> str:
return hashlib.md5(text.encode()).hexdigest()
def start(self):
self._running = True
thread = threading.Thread(target=self._poll, daemon=True)
thread.start()
def stop(self):
self._running = False
def _poll(self):
while self._running:
try:
text = pyperclip.paste()
if text and self._hash(text) != self.current_hash:
with self.lock:
self.current_text = text
self.current_hash = self._hash(text)
except Exception:
pass # clipboard may be locked briefly
time.sleep(self.poll_interval)
def get_latest(self) -> str:
with self.lock:
return self.current_text
Key decisions:
- MD5 hashing is fast and sufficient for change detection (we’re not securing anything).
- Threading keeps the clipboard poll from blocking the UI.
- 0.5-second poll balances responsiveness with CPU usage. You can drop to 0.2s if you want snappier detection.
Step 3: Communicating with the LLM
Create llm_client.py. This sends prompts to Ollama and handles responses.
import requests
import json
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "llama3.2:3b"
SYSTEM_PROMPTS = {
"summarize": "Summarize the following text in 2-3 concise sentences. Only return the summary.",
"translate": "Translate the following text to English. If it is already in English, return it unchanged. Only return the translation.",
"explain": "Explain the following text in simple terms, as if to a colleague who is new to the topic. Use 2-4 sentences.",
"rephrase": "Rephrase the following text to be clearer and more professional. Only return the rephrased version."
}
def query_ollama(text: str, action: str) -> str:
system_prompt = SYSTEM_PROMPTS.get(action, SYSTEM_PROMPTS["summarize"])
payload = {
"model": MODEL,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text}
],
"stream": False
}
response = requests.post(OLLAMA_URL, json=payload, timeout=60)
response.raise_for_status()
data = response.json()
return data["message"]["content"].strip()
We use stream: False for simplicity. If you want a streaming “typewriter” effect in the UI, switch to stream: True and iterate over newline-delimited JSON chunks—but that adds complexity we don’t need for a first build.
Step 4: The Pop-up UI with Tkinter
Create popup_ui.py. This is the window that appears on hotkey press.
import tkinter as tk
from tkinter import scrolledtext, ttk
import threading
class ClipboardPopup:
def __init__(self, watcher, llm_query_func):
self.watcher = watcher
self.llm_query = llm_query_func
self.cache = {}
self.window = None
def show(self):
if self.window and self.window.winfo_exists():
self.window.lift()
return
self.window = tk.Toplevel()
self.window.title("Smart Clipboard")
self.window.geometry("500x400")
self.window.attributes('-topmost', True)
text = self.watcher.get_latest()
preview = text[:200] + "..." if len(text) > 200 else text
tk.Label(self.window, text="Clipboard Content:", font=("Arial", 10, "bold")).pack(pady=(10,0))
tk.Label(self.window, text=preview, wraplength=480, justify="left").pack(pady=(0,10))
btn_frame = tk.Frame(self.window)
btn_frame.pack(pady=5)
actions = ["Summarize", "Translate", "Explain", "Rephrase"]
for action in actions:
ttk.Button(btn_frame, text=action,
command=lambda a=action.lower(): self._run_action(a)).pack(side="left", padx=5)
self.output_area = scrolledtext.ScrolledText(self.window, wrap=tk.WORD, width=58, height=15)
self.output_area.pack(pady=10, padx=10)
self.output_area.insert(tk.END, "Choose an action above...")
self.output_area.config(state=tk.DISABLED)
self.window.protocol("WM_DELETE_WINDOW", self._close)
def _run_action(self, action: str):
text = self.watcher.get_latest()
cache_key = f"{action}:{text}"
if cache_key in self.cache:
self._display_result(self.cache[cache_key])
return
self._display_result("Processing...")
thread = threading.Thread(target=self._query_and_display, args=(text, action, cache_key), daemon=True)
thread.start()
def _query_and_display(self, text, action, cache_key):
try:
result = self.llm_query(text, action)
self.cache[cache_key] = result
self._display_result(result)
except Exception as e:
self._display_result(f"Error: {str(e)}")
def _display_result(self, text):
if self.window and self.window.winfo_exists():
self.output_area.config(state=tk.NORMAL)
self.output_area.delete(1.0, tk.END)
self.output_area.insert(tk.END, text)
self.output_area.config(state=tk.DISABLED)
def _close(self):
self.window.destroy()
self.window = None
Note the cache dictionary—simple but effective. The _run_action method checks it before firing off an LLM request. This makes repeated actions on the same text instantaneous.
Step 5: Putting It All Together
Create main.py—the entry point that wires the daemon, hotkey listener, and UI.
from pynput import keyboard
from clipboard_daemon import ClipboardWatcher
from llm_client import query_ollama
from popup_ui import ClipboardPopup
import tkinter as tk
# Hotkey combination: Ctrl + Shift + Space
COMBINATION = {keyboard.Key.ctrl, keyboard.Key.shift, keyboard.Key.space}
current_keys = set()
def on_press(key):
try:
if key in COMBINATION:
current_keys.add(key)
if COMBINATION.issubset(current_keys):
popup.show()
except Exception:
pass
def on_release(key):
try:
current_keys.discard(key)
except Exception:
pass
if __name__ == "__main__":
# Start clipboard watcher
watcher = ClipboardWatcher(poll_interval=0.5)
watcher.start()
# Create hidden Tk root for the pop-up
root = tk.Tk()
root.withdraw() # hide the root window
popup = ClipboardPopup(watcher, query_ollama)
# Start hotkey listener
listener = keyboard.Listener(on_press=on_press, on_release=on_release)
listener.start()
print("Smart Clipboard running. Press Ctrl+Shift+Space to activate.")
root.mainloop()
How to Run Your Smart Clipboard
- Ensure Ollama is running (the app should be open, or run
ollama servein a terminal). - Install dependencies:
pip install pynput pyperclip requests - Run the daemon:
python main.py - Copy any text (Ctrl+C).
- Press Ctrl+Shift+Space.
- Click Summarize, Translate, Explain, or Rephrase.
The first query will take a few seconds as Ollama loads the model. Subsequent queries are faster. If you copy new text and hit the hotkey again, the pop-up shows the updated clipboard preview.
Sensible Extensions
Once this is running, you’re holding a local AI pipeline. Here’s where to take it next:
- Add a “Custom Prompt” field: Let users type arbitrary instructions. This turns the tool into a general-purpose text transformer.
- System tray minimization: Use
pystrayto keep the daemon in the system tray instead of a terminal window. - Multi-language translation: Replace the single “Translate to English” button with a dropdown of target languages.
- Clipboard history: Store the last N clipboard entries so you can transform text you copied earlier.
- Markdown rendering: If the LLM returns formatted text, render it in a
tkhtmlviewwidget.
This pattern—local daemon watching a data source, LLM processing on trigger—is the same one you’d use for log summarizers or document classifiers. If you’re hungry for more, Build an On-Call Incident Summarizer That Reads Logs and Drafts a Postmortem with Gemini walks through a production-style version of this idea.
Common Pitfalls
- Clipboard access on Linux:
pyperclipneedsxcliporxsel. Ifpyperclip.paste()returns empty, install one:sudo apt install xclip. - macOS permissions:
pynputrequires Accessibility access. Go to System Settings → Privacy & Security → Accessibility and add Terminal (or your IDE). - Ollama not reachable: If you get
ConnectionRefusedError, Ollama isn’t running. Start it withollama serveor open the desktop app. - Model too slow: Llama 3.2 3B runs on CPU, but if you have a GPU, Ollama uses it automatically. For even lighter weight, try
llama3.2:1b. - Tkinter thread safety: Tkinter isn’t thread-safe. We use
threadingfor LLM calls but update the UI via_display_result, which runs in the main thread becausepynputcallbacks happen there. If you refactor, keep UI updates on the main thread.
FAQ
Q: Can I use a different model?
Absolutely. Change the MODEL variable in llm_client.py to any model you’ve pulled with ollama pull. Mistral, Phi-3, and Gemma all work. Smaller models give faster responses; larger ones give better quality.
Q: Does this send my clipboard data anywhere?
No. Everything runs locally. Ollama communicates over localhost only. There’s no telemetry, no cloud round-trip.
Q: The pop-up appears but shows old clipboard text.
The daemon polls every 0.5 seconds. If you copy and immediately press the hotkey, the daemon may not have picked up the change yet. Wait a beat, or lower poll_interval to 0.2s.
Q: How do I change the hotkey?
Edit the COMBINATION set in main.py. pynput supports most key names—check the pynput docs.
Q: Can I package this as a standalone app?
Yes. Use PyInstaller to bundle the Python scripts and dependencies. You’ll need to include Tkinter explicitly on some platforms: pyinstaller --onefile --hidden-import tkinter main.py.
Q: What if I want this to work on images or PDFs?
This version handles text only. For images, you’d add OCR (Tesseract is free) before sending to the LLM. For PDFs, extract text with PyPDF2. The core architecture stays the same—you’re just enriching the input pipeline.
Building tools like this is the bread and butter of Forward Deployed Engineering: spot a repetitive cognitive task, wrap it in a thin AI layer, and ship it before lunch. If you want to go deeper into the FDE mindset, Scaling Yourself: When and How an FDE Hands Off to Core Engineering covers what happens when your little daemon turns into a company-wide feature.
Want to build like a Forward Deployed Engineer?
FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.
Explore the program