Build a Smart Clipboard That Translates & Summarizes with Groq + Piper TTS
What We're Building
A silent system tray utility that transforms your clipboard into a multilingual research assistant. You copy raw text—a dense paragraph from a German paper, a bloated email thread, a messy Slack dump—and the app instantly processes it using Groq’s free Llama inference. It can either summarize the text into three crisp bullets or translate it to English. The result is then spoken aloud by Piper TTS, a high-quality offline text-to-speech engine that runs entirely on your machine.
Feature Checklist
- Monitors system clipboard for new text entries
- Two modes, toggled from the tray: Summarize and Translate
- Groq API (free tier) for instant LLM inference—no GPU required
- Piper TTS for local, private speech synthesis—no network calls for audio
- Minimal system tray UI (pystray) with a single-click mode switch
- Hotkey-triggered processing (optional, covered in extensions)
This isn't a bloated Electron app. It’s ~250 lines of Python, fully offline for TTS, and costs $0 to run.
Architecture Deep Dive
Four decoupled components talk to each other through a simple event bus (a Python queue). The clipboard watcher pushes raw text in. The Groq engine transforms it. Piper speaks the result. The tray controller manages state and user intent.
The queue decouples I/O-bound clipboard polling from the LLM call, so a slow Groq response never blocks clipboard detection. Piper runs synchronously after Groq returns—TTS generation is fast enough (<500ms for a short paragraph) that async isn't worth the complexity here.
Prerequisites (All Free-Tier)
- Python 3.10+ — python.org/downloads
- Groq API Key — Sign up at console.groq.com. Free tier gives you generous requests per minute on
llama-3.1-8b-instant. - Piper TTS — Download a prebuilt binary and voice model from the Piper GitHub releases. We'll use the
en_US-lessac-mediumvoice (~50MB). - pip packages:
groq,pyperclip,pystray,Pillow(for tray icon),threading,queue(stdlib).
No Docker. No cloud TTS bills. No GPU.
Step 1: Project Scaffolding & Environment
mkdir smart-clipboard && cd smart-clipboard
python -m venv venv && source venv/bin/activate # or venv\Scripts\activate on Windows
pip install groq pyperclip pystray Pillow
Download Piper. We'll place the binary and voice model in a piper/ directory:
mkdir piper
# Linux example — adjust for your OS
wget https://github.com/rhasspy/piper/releases/latest/download/piper_linux_x86_64.tar.gz
tar -xzf piper_linux_x86_64.tar.gz -C piper/
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx -P piper/
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json -P piper/
Verify Piper works:
echo "System online." | ./piper/piper -m piper/en_US-lessac-medium.onnx --output-raw | aplay -r 22050 -f S16_LE -t raw - # Linux
# Windows/macOS: piper outputs a .wav file by default — just play it with your OS default.
Step 2: The Groq Inference Engine
Create engine.py. This module takes raw text and a mode flag, calls Groq, and returns the processed string.
import os
from groq import Groq
client = Groq(api_key=os.environ["GROQ_API_KEY"])
def process_text(text: str, mode: str = "summarize") -> str:
if mode == "summarize":
system_prompt = "Summarize the following text into exactly three bullet points. Be concise. Return only the bullets."
else:
system_prompt = "Translate the following text to English. If it's already in English, return it unchanged. Return only the translation."
response = client.chat.completions.create(
model="llama-3.1-8b-instant",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": text}
],
temperature=0.3,
max_tokens=300
)
return response.choices[0].message.content.strip()
Why llama-3.1-8b-instant? It's the fastest model on Groq's free tier. For summarization and translation, 8B parameters is overkill—the speed gain vs. a 70B model is worth the marginal quality tradeoff.
Step 3: Integrating Piper TTS
Create speaker.py. We shell out to Piper because its Python bindings are still maturing. This is a pragmatic choice—the subprocess overhead is negligible compared to LLM latency.
import subprocess
import tempfile
import os
PIPER_BINARY = os.path.join(os.path.dirname(__file__), "piper/piper")
PIPER_MODEL = os.path.join(os.path.dirname(__file__), "piper/en_US-lessac-medium.onnx")
def speak(text: str) -> None:
# Piper reads from stdin, writes WAV to stdout with --output-raw
# We'll write to a temp file and play it for cross-platform simplicity
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
wav_path = f.name
try:
cmd = [PIPER_BINARY, "-m", PIPER_MODEL, "--output_file", wav_path]
subprocess.run(cmd, input=text.encode(), capture_output=True, check=True)
# Cross-platform playback
if os.name == "nt":
import winsound
winsound.PlaySound(wav_path, winsound.SND_FILENAME)
elif os.uname().sysname == "Darwin":
subprocess.run(["afplay", wav_path])
else:
subprocess.run(["aplay", wav_path])
finally:
os.unlink(wav_path)
Step 4: The Clipboard Watchdog
Create watcher.py. We poll the clipboard every 0.5 seconds using pyperclip. When text changes (and isn't empty), we push it onto the shared queue.
import threading
import time
import queue
import pyperclip
def clipboard_watcher(q: queue.Queue, stop_event: threading.Event):
recent = ""
while not stop_event.is_set():
try:
current = pyperclip.paste()
if current != recent and current.strip():
q.put(current.strip())
recent = current
except pyperclip.PyperclipException:
pass # clipboard locked or unavailable
time.sleep(0.5)
Why polling? True OS-level clipboard hooks require platform-specific C extensions (Win32 API, NSPasteboard, X11 selections). Polling at 500ms is imperceptible to users and keeps the code fully cross-platform.
Step 5: The System Tray Controller
Create tray_app.py. This is the UI—a single icon in the system tray with a menu to toggle mode and quit. We use pystray with a simple 16x16 PNG icon (generate one with Pillow or download a free clipboard icon).
import threading
import queue
from PIL import Image, ImageDraw
import pystray
def create_icon_image():
# Generate a simple 64x64 clipboard icon
img = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.rounded_rectangle([8, 8, 56, 56], radius=8, fill="#4A90D9")
return img
class TrayController:
def __init__(self, q: queue.Queue, stop_event: threading.Event):
self.q = q
self.stop_event = stop_event
self.mode = "summarize"
self.icon = pystray.Icon(
"smart_clipboard",
create_icon_image(),
"Smart Clipboard",
menu=pystray.Menu(
pystray.MenuItem("Mode: Summarize", self.toggle_mode, checked=lambda item: self.mode == "summarize"),
pystray.MenuItem("Mode: Translate", self.toggle_mode, checked=lambda item: self.mode == "translate"),
pystray.Menu.SEPARATOR,
pystray.MenuItem("Quit", self.quit)
)
)
def toggle_mode(self, icon, item):
self.mode = "translate" if self.mode == "summarize" else "summarize"
# Push a mode-change sentinel so the processor updates immediately
self.q.put(("__MODE__", self.mode))
def quit(self, icon, item):
self.stop_event.set()
icon.stop()
def run(self):
self.icon.run()
Step 6: Wiring Everything Together
Create main.py. The orchestrator starts the clipboard watcher thread, the tray icon (which runs on the main thread by pystray convention), and a processor thread that consumes from the queue.
import threading
import queue
import os
from watcher import clipboard_watcher
from engine import process_text
from speaker import speak
from tray_app import TrayController
def processor(q: queue.Queue, tray: TrayController, stop_event: threading.Event):
while not stop_event.is_set():
try:
item = q.get(timeout=0.5)
if isinstance(item, tuple) and item[0] == "__MODE__":
tray.mode = item[1]
continue
raw_text = item
print(f"Processing ({tray.mode}): {raw_text[:80]}...")
result = process_text(raw_text, tray.mode)
print(f"Result: {result}")
speak(result)
except queue.Empty:
continue
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
if "GROQ_API_KEY" not in os.environ:
raise RuntimeError("Set GROQ_API_KEY environment variable")
q = queue.Queue()
stop_event = threading.Event()
tray = TrayController(q, stop_event)
watcher_thread = threading.Thread(target=clipboard_watcher, args=(q, stop_event), daemon=True)
processor_thread = threading.Thread(target=processor, args=(q, tray, stop_event), daemon=True)
watcher_thread.start()
processor_thread.start()
# tray.run() blocks until quit
tray.run()
How to Run It
export GROQ_API_KEY="gsk_your_key_here"
python main.py
The clipboard icon appears in your system tray. Right-click to switch between Summarize and Translate. Copy any text—a news article, a foreign-language paragraph, a long email—and within seconds you'll hear the processed result spoken aloud.
First-run latency note: The first Groq call may take 2-3 seconds as the model warms up. Subsequent calls are sub-second.
Sensible Extensions
This foundation is deliberately minimal. Here's where to take it next, ordered by effort-to-value ratio:
- Hotkey Trigger — Use
pynputto listen for a global hotkey (e.g., Ctrl+Shift+S) that explicitly triggers processing, rather than auto-processing every copy. Ideal for sensitive workflows. - Notification Popup — Use
plyerto show a desktop notification with the processed text before speaking it. Gives you a visual preview. - Multi-Language TTS — Swap the Piper voice model based on detected language. Piper has 30+ languages. Groq can return a language code alongside the translation.
- Clipboard History — Keep a rotating buffer of the last 20 processed items, accessible from the tray menu. See our guide on building a local RAG chatbot over personal documents for ideas on local storage patterns.
- PR Review Mode — Add a mode that takes a copied code diff and generates a review comment. We covered the full architecture for this in our GitHub PR review bot guide.
Common Pitfalls & Debugging
- Piper binary not found: Ensure the binary path in
speaker.pyis correct. On macOS, you may need to runchmod +x piper/piperand allow it in Security & Privacy. - Groq rate limits: Free tier is generous but not infinite. If you get 429 errors, add a 1-second sleep in the processor or batch clipboard changes.
- Clipboard not detecting: Some Linux Wayland compositors restrict clipboard access. Run with
QT_QPA_PLATFORM=xcbor switch to X11.pyperclipworks best on X11 and Windows. - TTS sounds robotic: The
lessac-mediumvoice is a good balance of quality and speed. For more natural speech, downloaden_US-libritts-high(~300MB) and update the model path. - pystray icon doesn't appear on Linux: Install
libappindicator3-devorlibayatana-appindicator3-devdepending on your distro.
FAQ
Q: Can I use a different free LLM provider?
Yes. Swap the Groq client for OpenAI's free tier (gpt-3.5-turbo), Anthropic's free Claude tier, or a local Ollama model. The architecture doesn't care. If you're interested in local-only pipelines, check out how we built a RAG chatbot over personal PDFs with Ollama.
Q: Why not use the system TTS engine? System TTS (say on macOS, SAPI on Windows) sends your text to the OS, which may phone home or have inconsistent quality. Piper is fully offline, deterministic, and sounds remarkably good for a 50MB model.
Q: How do I make this start on boot?
Add a .desktop file on Linux (~/.config/autostart/), a shortcut in Windows Startup folder, or a LaunchAgent on macOS. The tray icon makes it unobtrusive enough to run continuously.
Q: Is the text sent to Groq private? Groq's free tier is subject to their standard data policy. For sensitive text, consider a local model via Ollama. The screenshot-to-React agent guide uses a similar pattern with Google Gemini—the same privacy considerations apply.
Q: Can I process images or PDFs from the clipboard? Not with this build. But the architecture supports it: add a detection step for MIME types in the watcher, and route images to a vision model. We've done something similar in the lead enrichment agent that researches companies.
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