All articles
Build Guides

Build a Smart Clipboard That Translates & Summarizes with Groq + Piper TTS

FDE Coach EditorialAugust 6, 20269 min read

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-medium voice (~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:

  1. Hotkey Trigger — Use pynput to listen for a global hotkey (e.g., Ctrl+Shift+S) that explicitly triggers processing, rather than auto-processing every copy. Ideal for sensitive workflows.
  2. Notification Popup — Use plyer to show a desktop notification with the processed text before speaking it. Gives you a visual preview.
  3. 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.
  4. 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.
  5. 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.py is correct. On macOS, you may need to run chmod +x piper/piper and 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=xcb or switch to X11. pyperclip works best on X11 and Windows.
  • TTS sounds robotic: The lessac-medium voice is a good balance of quality and speed. For more natural speech, download en_US-libritts-high (~300MB) and update the model path.
  • pystray icon doesn't appear on Linux: Install libappindicator3-dev or libayatana-appindicator3-dev depending 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.

#clipboard#summarization#tts#automation

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