All articles
Build Guides

Build a Smart Clipboard: Summarize & Translate Anything You Copy with Ollama

FDE Coach EditorialJuly 24, 202610 min read

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.

ToolPurposeInstall / Link
Python 3.10+Runtimepython.org/downloads
OllamaLocal LLM serverollama.com/download
Llama 3.2 3BLightweight modelollama pull llama3.2:3b
pynputGlobal hotkey listenerpip install pynput
pyperclipClipboard accesspip install pyperclip
requestsHTTP calls to Ollamapip install requests
TkinterGUI (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

  1. Ensure Ollama is running (the app should be open, or run ollama serve in a terminal).
  2. Install dependencies:
    pip install pynput pyperclip requests
    
  3. Run the daemon:
    python main.py
    
  4. Copy any text (Ctrl+C).
  5. Press Ctrl+Shift+Space.
  6. 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 pystray to 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 tkhtmlview widget.

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: pyperclip needs xclip or xsel. If pyperclip.paste() returns empty, install one: sudo apt install xclip.
  • macOS permissions: pynput requires 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 with ollama serve or 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 threading for LLM calls but update the UI via _display_result, which runs in the main thread because pynput callbacks 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.

#clipboard#ollama#local-ai#productivity

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

More build guides

August 15 · 0d left
Enroll Now