Build a Smart Clipboard That Summarizes and Translates Anything You Copy with Gemini
What We’re Building
A lightweight system-tray clipboard manager that watches your clipboard. Copy any text—a dense research abstract, a long error log, a paragraph in German—and within a second a popup appears with a crisp summary and an English translation. Click one button to replace your clipboard with the summary, another to copy the translation, or dismiss and keep working.
Feature list:
- System-tray resident app – runs silently in the background, zero terminal windows required.
- Clipboard polling – detects new text copies using
pyperclipwith a hash-based change detector. - Gemini-powered summarization – condenses long text into 2–3 bullet points using Google’s free-tier
gemini-1.5-flashmodel. - Gemini-powered translation – auto-detects source language and translates to English (or a target language you configure).
- Non-blocking popup – a Tkinter
Toplevelwindow appears near the cursor, never steals focus from your active window. - One-click actions – “Copy Summary,” “Copy Translation,” or “Dismiss.”
- Configurable model and language – swap models or change the target language with a single constant in the code.
This is an engineer’s utility, not a product. It’s ~250 lines of Python, uses only free-tier APIs, and teaches you how to compose a local listener with a remote LLM without over-engineering the bridge.
Architecture Overview
The poller thread runs every 500 ms. When it detects a new text payload (hash differs from the last processed one), it fires a non-blocking call to Gemini. The response populates a Tkinter popup that appears near the mouse pointer. All clipboard writes happen only on explicit user action, so we never accidentally overwrite the clipboard with intermediate results.
Prerequisites
Everything here is free-tier or open-source:
- Python 3.10+ – python.org/downloads
- Google Gemini API key – grab one at aistudio.google.com/app/apikey. The free tier gives you 15 requests per minute on
gemini-1.5-flash, which is plenty for a clipboard watcher. - Python packages (install via
pip):pip install pyperclip google-generativeai pystray pillowpyperclip– cross-platform clipboard access.google-generativeai– official Gemini SDK.pystray– system-tray icon (usesPIL/Pillowunder the hood).pillow– required bypystrayfor icon rendering.
No GPU, no Docker, no paid API tiers. If you have a laptop and Wi-Fi, you can run this.
Step 1: Scaffold the Tkinter System Tray App
We need a root Tkinter window that we immediately hide, a system-tray icon, and a clean shutdown path.
import tkinter as tk
import threading
from pystray import Icon, Menu, MenuItem
from PIL import Image, ImageDraw
# Create a simple 64x64 icon
def create_icon_image():
img = Image.new("RGB", (64, 64), "white")
draw = ImageDraw.Draw(img)
draw.rectangle([16, 16, 48, 48], fill="#4285F4") # Google blue
return img
class ClipboardApp:
def __init__(self):
self.root = tk.Tk()
self.root.withdraw() # Hide the main window
self.running = True
def run(self):
icon_image = create_icon_image()
menu = Menu(MenuItem("Quit", self.quit_app))
self.icon = Icon("smart_clipboard", icon_image, "Smart Clipboard", menu)
threading.Thread(target=self.icon.run, daemon=True).start()
self.root.mainloop()
def quit_app(self):
self.running = False
self.icon.stop()
self.root.quit()
if __name__ == "__main__":
app = ClipboardApp()
app.run()
Run this and you’ll see a blue square in your system tray. Right-click → Quit stops the app. The root.mainloop() call blocks the main thread, which is fine because we’ll run the clipboard poller on a separate daemon thread.
Step 2: Poll the Clipboard for Changes
We poll every 500 ms, comparing a SHA-256 hash of the clipboard content against the last processed hash. This avoids re-triggering on the same text and ignores non-text clipboard formats.
import hashlib
import time
import pyperclip
class ClipboardApp:
def __init__(self):
# ... previous init ...
self.last_hash = None
self.processing = False
self.poll_thread = threading.Thread(target=self._poll_loop, daemon=True)
def _poll_loop(self):
while self.running:
try:
text = pyperclip.paste()
if text and isinstance(text, str):
text_hash = hashlib.sha256(text.encode()).hexdigest()
if text_hash != self.last_hash and not self.processing:
self.last_hash = text_hash
self.processing = True
self._handle_new_text(text)
except Exception:
pass # Clipboard unavailable or non-text
time.sleep(0.5)
Key design decisions:
self.processingflag prevents overlapping requests if the API call takes longer than 500 ms.- We catch all exceptions in the poll loop so a transient clipboard error doesn’t kill the thread.
- Hash comparison is cheap and avoids string-length checks that fail on minor edits.
Step 3: Wire Up Gemini for Summarization and Translation
We’ll send a single prompt asking for both a summary and a translation, then parse the structured response. This keeps us under the free-tier rate limit by using one API call instead of two.
import google.generativeai as genai
genai.configure(api_key="YOUR_GEMINI_API_KEY")
class ClipboardApp:
def __init__(self):
# ... previous init ...
self.model = genai.GenerativeModel("gemini-1.5-flash")
self.target_language = "English"
def _summarize_and_translate(self, text):
prompt = f"""You are a clipboard assistant. Given the following text, do two things:
1. Write a concise summary in 2-3 bullet points.
2. Translate the FULL original text into {self.target_language}.
Format your response EXACTLY like this:
---SUMMARY---
• bullet 1
• bullet 2
---TRANSLATION---
translated text here
Text to process:
{text}"""
response = self.model.generate_content(prompt)
return response.text
def _parse_response(self, raw):
summary = ""
translation = ""
if "---SUMMARY---" in raw and "---TRANSLATION---" in raw:
parts = raw.split("---TRANSLATION---")
summary_part = parts[0].replace("---SUMMARY---", "").strip()
translation = parts[1].strip()
summary = summary_part
else:
# Fallback: treat whole response as summary
summary = raw
return summary, translation
Why gemini-1.5-flash? It’s fast (typically <1 s latency for short-to-medium texts), free-tier eligible, and handles multilingual input natively. If you need higher quality, swap to gemini-1.5-pro—just watch your free-tier quota.
Step 4: Build the Popup UI and Action Buttons
The popup must appear near the cursor, stay on top without stealing focus, and offer three actions: copy summary, copy translation, dismiss.
import tkinter as tk
class ClipboardApp:
def _show_popup(self, summary, translation):
popup = tk.Toplevel(self.root)
popup.title("Smart Clipboard")
popup.geometry("450x300")
popup.attributes("-topmost", True)
popup.overrideredirect(True) # Remove title bar for clean look
# Position near cursor
x, y = popup.winfo_pointerxy()
popup.geometry(f"+{x+10}+{y+10}")
# Summary section
tk.Label(popup, text="Summary:", font=("Arial", 10, "bold")).pack(anchor="w", padx=10, pady=(10, 0))
summary_text = tk.Text(popup, height=6, wrap="word", font=("Arial", 9))
summary_text.insert("1.0", summary)
summary_text.config(state="disabled")
summary_text.pack(fill="x", padx=10, pady=5)
# Translation section
tk.Label(popup, text=f"Translation ({self.target_language}):", font=("Arial", 10, "bold")).pack(anchor="w", padx=10)
trans_text = tk.Text(popup, height=6, wrap="word", font=("Arial", 9))
trans_text.insert("1.0", translation)
trans_text.config(state="disabled")
trans_text.pack(fill="x", padx=10, pady=5)
# Button frame
btn_frame = tk.Frame(popup)
btn_frame.pack(fill="x", padx=10, pady=10)
tk.Button(btn_frame, text="Copy Summary",
command=lambda: self._copy_and_dismiss(summary, popup)).pack(side="left", padx=5)
tk.Button(btn_frame, text="Copy Translation",
command=lambda: self._copy_and_dismiss(translation, popup)).pack(side="left", padx=5)
tk.Button(btn_frame, text="Dismiss", command=popup.destroy).pack(side="right", padx=5)
def _copy_and_dismiss(self, text, popup):
pyperclip.copy(text)
self.last_hash = hashlib.sha256(text.encode()).hexdigest()
popup.destroy()
self.processing = False
The _copy_and_dismiss method updates last_hash so the poller doesn’t immediately re-trigger on the text we just placed on the clipboard. This is a subtle but critical detail—without it, copying the summary would cause an infinite loop of API calls.
Step 5: Assemble the Main Loop and State Machine
The final _handle_new_text method ties everything together. It runs the Gemini call in a thread to avoid blocking the Tkinter event loop, then schedules the popup creation on the main thread.
class ClipboardApp:
def _handle_new_text(self, text):
def task():
try:
raw = self._summarize_and_translate(text)
summary, translation = self._parse_response(raw)
self.root.after(0, lambda: self._show_popup(summary, translation))
except Exception as e:
self.root.after(0, lambda: self._show_error(str(e)))
threading.Thread(target=task, daemon=True).start()
def _show_error(self, msg):
popup = tk.Toplevel(self.root)
popup.title("Error")
popup.geometry("300x100")
popup.attributes("-topmost", True)
tk.Label(popup, text=f"Gemini API error:\n{msg[:200]}", fg="red").pack(padx=20, pady=20)
tk.Button(popup, text="OK", command=lambda: [popup.destroy(), setattr(self, 'processing', False)]).pack()
Full run() method with the poll thread started:
def run(self):
icon_image = create_icon_image()
menu = Menu(MenuItem("Quit", self.quit_app))
self.icon = Icon("smart_clipboard", icon_image, "Smart Clipboard", menu)
threading.Thread(target=self.icon.run, daemon=True).start()
self.poll_thread.start()
self.root.mainloop()
Running the Clipboard Manager
- Save the complete script as
smart_clipboard.py. - Replace
YOUR_GEMINI_API_KEYwith your actual key from aistudio.google.com. - Install dependencies:
pip install pyperclip google-generativeai pystray pillow - Run:
python smart_clipboard.py - Look for the blue icon in your system tray. Copy any text—a news article, a GitHub issue, a foreign-language email—and watch the popup appear.
On macOS, you may need to grant Accessibility permissions to your terminal or IDE. On Linux, ensure you have libxcb and libappindicator installed for system-tray support.
Extensions and Customizations
Once the core loop works, the composable architecture makes it easy to bolt on new capabilities:
| Extension | What to Change |
|---|---|
| Tone/style presets | Add a dropdown to the popup that modifies the prompt: “summarize as a tweet,” “explain like I’m five,” “extract action items.” |
| Clipboard history | Store (hash, summary, translation, timestamp) in a SQLite DB. Add a “History” menu item that opens a scrollable list. |
| Multi-target translation | Let the user pick target language from a right-click tray menu; update self.target_language dynamically. |
| Hotkey trigger | Instead of auto-triggering on every copy, only process text when the user presses a configurable hotkey (use pynput or keyboard library). |
| Offline fallback | If Gemini is unreachable, fall back to a local model via ollama (e.g., llama3.2:1b). Check connectivity first, then route accordingly. |
For engineers interested in building more LLM-powered workflow tools, our Build an On-Call Incident Summarizer That Reads Logs and Drafts a Postmortem with Groq guide applies a similar polling-LLM-popup pattern to production logs. If you’re thinking about how these automation skills map to a career in technical customer engineering, Essential Skills for a Forward Deployed Engineer: Technical and Soft Proficiencies breaks down exactly what makes an FDE effective.
Common Pitfalls and Debugging Tips
Clipboard doesn’t trigger on first copy. pyperclip sometimes returns stale data on the first paste() call. Add a small warm-up read in __init__:
_ = pyperclip.paste() # Prime the clipboard
Popup appears but is empty. Gemini’s structured output parsing is brittle. If the model returns ---SUMMARY--- with different casing or spacing, you’ll get empty strings. Add logging:
print(f"Raw response:\n{raw}") # Debug: remove in production
Then adjust _parse_response to handle variations. Better yet, switch to Gemini’s controlled generation (JSON mode) once you’re comfortable with the prompt shape.
System tray icon doesn’t appear on Linux. Install libappindicator3-dev and gir1.2-appindicator3-0.1, then pip install pystray again. Some desktop environments (KDE, LXQt) need additional tray support packages.
Rate limit errors from Gemini. The free tier allows 15 RPM. If you copy text rapidly, you’ll hit this. Add a cooldown:
time.sleep(4) # Enforce 15 RPM max
inside _handle_new_text before calling the API.
Popup steals focus. On Windows, overrideredirect(True) windows can still grab focus. Try popup.wm_attributes("-topmost", True) and avoid calling popup.focus_force(). On macOS, use popup.attributes("-transient", self.root) to keep the popup tied to the hidden root window.
FAQ
Q: Does this work on macOS, Windows, and Linux?
A: Yes. pyperclip and pystray are cross-platform. You may need minor OS-specific tweaks for tray icon behavior (see pitfalls above).
Q: Is my text sent to Google’s servers? A: Yes, the text you copy is sent to the Gemini API endpoint. Google’s data usage policy for the free tier states they do not use your prompts for training, but review the terms if you’re handling sensitive data.
Q: Can I use a different LLM?
A: Absolutely. Swap the google-generativeai SDK for openai (GPT-3.5/4o-mini free credits), groq (Llama models, free tier), or a local Ollama endpoint. The architecture stays identical—only the _summarize_and_translate method changes.
Q: How do I package this as a standalone executable?
A: Use pyinstaller:
pip install pyinstaller
pyinstaller --onefile --windowed --add-data "icon.png:." smart_clipboard.py
This produces a single .exe (Windows) or .app bundle (macOS) that non-technical users can run.
Q: What if I want to build more agentic tools like this? A: The pattern here—local listener, remote LLM call, non-blocking UI—is the foundation for dozens of productivity tools. Check out our Build a Daily Standup Bot That Collects Updates via Slack and Posts a Summary with Groq guide to see how the same architecture extends to team workflows. For a deeper dive into how browser agents are reshaping tool design, Kimi K3 Tops the Frontend Code Arena: How Browser Agents Are Actually Scored is worth a read.
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