Project Info
Inspiration
Tokens are the meter on every LLM bill, and almost all of them are waste. Spoken language, retrieved RAG context, and long documents are massively redundant — but the redundancy lives in different places. Some is low-information tokens ("um, so basically the idea is…"), some is irrelevant passages that a query never touches, and some is the sheer bit-width of the KV cache the model carries while it generates. No single paper kills all three. So instead of picking one compression technique and hoping, we built Winnow: a pipeline that runs several state-of-the-art compressors in parallel, merges their decisions, and then — optionally — hands the result to an LLM whose KV cache is itself compressed. Every stage is independently justified by a recent paper; the contribution is making them compose. The Product: showcasing use cases of compression in "Test" -> users can upload multiple PDFs or talk and witness our hard-token pruning themselves. playground: tweak and play with our compression algorithm and compare and find the best one that works for your use case. There's plenty of variety, idk how we managed to get all these methods in and working in 24 hours lol.
What it does
(compression-specific) We tried our best to include every strong, parallelizable and patchable compression method we could find — and where no public repo existed, we re-implemented the paper ourselves from scratch (realistically, only LLMLingua shipped usable code). The papers Winnow builds on: LLMLingua-2 — Data Distillation for Efficient and Faithful Task-Agnostic Prompt Compression — arXiv:2403.12968 · repo LongLLMLingua — Accelerating and Enhancing LLMs in Long Context Scenarios via Prompt Compression — arXiv:2310.06839 · repo AttentionRAG — Attention-Guided Context Pruning in Retrieval-Augmented Generation — arXiv:2503.10720 (no public repo — self-implemented) LCLM — End-to-End Context Compression at Scale — arXiv:2606.09659 · repo TurboQuant — Online Vector Quantization with Near-Optimal Distortion Rate (KV-cache compression) — arXiv:2504.19874 · reference impl CompactPrompt — A Unified Pipeline for Prompt & Data Compression in LLM Workflows — arXiv:2510.18043 (explored; no public dataset to reproduce its style metric) Winnow compresses a prompt along two orthogonal axes and lets the user choose how far to push each one. 1. Token-space compression (which words survive). Given a piece of text, Winnow fans it out to two independent compressor families at once: LLMLingua-2 + LongLLMLingua. LLMLingua-2 is an extractive token classifier (a fine-tuned XLM-RoBERTa that labels each token keep/drop). LongLLMLingua adds the query-aware arm: a BGE cross-encoder reranker does coarse, question-aware document selection and reordering (to fight "lost-in-the-middle"), then the causal perplexity path compresses the survivors conditioned on the question. AttentionRAG. A faithful implementation of Attention-Guided Context Pruning in RAG (arXiv:2503.10720). It reformulates the query into an incomplete-answer template with a single "focal" blank, runs next-token prediction over each context chunk, and uses the focal token's summed-over-all-layers attention to rank tokens — keeping the sentences that hold the top-k. Chunks the model answers none on are dropped entirely. This requires a question to be run only when we are querying context (users are invited to try this in the Learn tab). Both arms produce per-token keep decisions. Winnow then merges them token-by-token over the original text, preserving chronology, under a boolean rule the user picks: intersection (keep a word only if both methods kept it — maximally aggressive) or union (keep if either did — safer, recall-oriented). The merge is a true alignment to one canonical token sequence (LLMLingua-2's word labels), not a fragile string-membership test, with a fallback to LLMLingua-only if AttentionRAG gates everything out so we never wipe the text. 2. Model-space compression (how the answer is generated). Once Winnow has the compressed string, the user chooses where it goes: Black box. Send the compressed prompt straight to a hosted API — Anthropic Claude or OpenAI GPT, picked per request. The savings are immediate and provider-agnostic, because the compressed prompt is just text. Self-hosted + TurboQuant. Route to our own Qwen model on Modal, generating with a TurboQuant-compressed KV cache (Google Research, ICLR 2026, arXiv:2504.19874) — a data-free random-rotation quantizer that drops the KV cache to ~4 bits (3–4× smaller) as a DynamicCache subclass, validated on Mistral-7B and Qwen2.5-14B. Self-hosted + LCLM + TurboQuant. Route to LCLM (End-to-End Context Compression at Scale, latent-context's released encoder→adapter→decoder checkpoints, ~2 weeks old at build time). LCLM compresses the long context into a handful of latent soft tokens that the decoder consumes as input embeddings — shrinking the KV cache's sequence length. TurboQuant then compresses the bits per entry of that same decoder cache. The two are orthogonal, so the savings multiply: fewer KV entries × fewer bits each. The net effect: token-level pruning before the model, sequence-length and bit-width compression inside it — three independent papers stacked end to end.
How we built it
Compression workers on Modal GPUs. LLMLingua-2/LongLLMLingua, AttentionRAG, TurboQuant, and LCLM+TurboQuant each run as a warm Modal A100 worker, tied to a FastAPI server's lifecycle so models load once at startup and real requests pay no cold-start cost. /compress fans LLMLingua and AttentionRAG out concurrently and merges; /generate routes between the Qwen-TurboQuant and LCLM-TurboQuant workers on an lclm flag. The merge engine (token_merge.py) is pure Python: it normalizes LLMLingua's labels, reconstructs each word's char-span in the original by an in-order forward scan, tests AttentionRAG's kept sentence-spans by char overlap, applies the union/intersection rule, and splices survivors back together preserving original spacing. TurboQuant is a drop-in DynamicCache subclass — Lloyd-Max Gaussian quantization with an optional outlier-channel path — so it slots straight in as any HF model's past_key_values, including the LCLM decoder. Frontend is a Next.js app that shows the raw vs. compressed diff live, token counts, % and $ saved, and an A/B Q&A box that asks the same question against both prompts to prove meaning survived.
Challenges we ran into
Pairing TurboQuant with LCLM. Getting TurboQuant's low-bit (down to ~3.5-bit outlier) quantization to drop cleanly into LCLM's decoder was the hardest integration. LCLM feeds the decoder inputs_embeds (the soft tokens), not token ids, and we had to confirm our TQCache survives that path and the encoder's single prefill without corrupting the latent memory block. Inventing the merge algorithm. Unionizing/intersecting two different compressors is not a set operation on strings — the two methods segment text differently. We had to align both to one canonical token sequence and operate on char-spans so the merge is order-preserving and unambiguous even with repeated words, plus a fallback so an empty AttentionRAG result never erases everything. Tuning LongLLMLingua. Choosing the right lead anchors / causal backbone and reranker for the question-aware arm took real iteration — what to condition on, how to reorder context against position bias, and where the causal path actually beats the cheaper extractive classifier. CompactPrompt had no public data. We tried to implement CompactPrompt's style-metric approach, but the paper shipped no public datasets to fit/evaluate the metric against, so we couldn't reproduce it faithfully and left it out rather than ship something unvalidated. Accomplishments we're proud of A working two-axis compressor: token-space (LLMLingua ∪/∩ AttentionRAG) and model-space (LCLM × TurboQuant) stacked in one pipeline. A genuinely novel merge between two heterogeneous compression algorithms that stays order-preserving and never destroys the text. TurboQuant validated end-to-end (3–4× KV compression, output matching FP16) and composing with LCLM so the KV savings multiply.
What we learned
The big wins come from compressing along different axes and stacking them — token count, sequence length, and bit-width are independent levers. Heterogeneous methods need a common canonical representation before you can combine them; alignment, not set math, is the real work. Faithfully reproducing a paper is gated by its artifacts: no public dataset (CompactPrompt) effectively means no faithful reimplementation.
What's next
Learned (rather than user-picked) routing between intersection/union and between black-box vs. self-hosted, per prompt and budget. Custom CUDA kernels for TurboQuant to turn the memory win into a latency win. Revisiting CompactPrompt if/when evaluation data becomes available. Built with Modal · FastAPI · Next.js · LLMLingua-2 · LongLLMLingua · AttentionRAG (arXiv:2503.10720) · LCLM (End-to-End Context Compression at Scale) · TurboQuant (arXiv:2504.19874) · BGE rerankers · Qwen · Anthropic Claude · OpenAI GPT · Deepgram
Winnow
Voice-first, real-time token compression for LLM pipelines. Speak, and we transcribe, prune, and pipe — losing words, keeping meaning.
Winnow is a hackathon project that compresses live speech transcripts in real time before they reach an LLM, demonstrably without losing meaning. Spoken language is verbose; LLM input tokens cost money. Winnow shows you the savings, the diff, and a side-by-side fidelity audit, then lets you talk to the compressed transcript through a Claude-Projects-style workspace.
What it does
Two views, one pipeline.
Compare
Watch compression happen live. Raw transcript on the left as you speak, pruned transcript on the right within milliseconds of each pause. Pruned words are struck through. A stats bar tracks running token counts, % saved, $ saved per the chosen LLM, average pipeline latency, and a sparkline of compression ratio over time. A built-in A/B Q&A box asks the same question against both transcripts in parallel — when the answers match, you have proof compression preserved the buried details.
Learn
A voice-first Claude-Projects-style workspace built around the compressed transcript. Tap the mic, talk naturally, each pause auto-sends a chat message. Claude streams answers back. One-tap Insights generate a summary, decision log, action items, flashcards (flippable carousel), and glossary — all backed by Anthropic prompt caching so multi-turn sessions stay cheap.
Stage-safety features
- Swappable input source. Live mic and a pre-recorded JSON fixture both implement the same
TranscriptSourceinterface. One click swaps them; downstream code can't tell which is active. If the live mic ever fails on stage, the fallback is a literal replay of you doing the demo, with original utterance timing. - In-app fixture recorder. Capture a live session, download as JSON, drop into
public/fixtures/demo-transcript.json— and your fallback becomes you. - Director mode. Keyboard shortcuts (
Spacestart/stop,Rswap source,1/2/3fire preset Q&A probes,Xclear,?show overlay) let you drive the demo without touching a trackpad.
Compression pipeline
Winnow compresses along two orthogonal axes: token-space (which words survive, before the model) and model-space (sequence length + KV-cache bit-width, inside the model). The same input is fanned out to several papers in parallel, their keep-decisions are merged token-by-token, and the user picks how the compressed prompt is finally answered.
flowchart TD
A[Raw text / transcript] --> B[Compression stage]
subgraph PAR[Parallel compressors over the same input]
direction LR
C[LLMLingua-2<br/>extractive token classifier]
D[LongLLMLingua<br/>query-aware perplexity<br/>+ BGE reranker]
E[AttentionRAG<br/>attention-guided<br/>sentence pruning]
end
B --> C
B --> D
B --> E
C --> F[LLMLingua keep-mask]
D --> F
E --> G[AttentionRAG keep-spans]
F --> H{Token-by-token merge<br/>over original text}
G --> H
H -->|intersection: both kept| I[Compressed prompt]
H -->|union: either kept| I
I --> J{User routing}
J -->|Black box| K[Anthropic Claude /<br/>OpenAI GPT API]
J -->|Self-hosted + TurboQuant| L[Qwen + TurboQuant<br/>KV-cache bit quantization]
J -->|Self-hosted + LCLM| M[LCLM encoder→decoder<br/>soft tokens<br/>+ TurboQuant on decoder KV]
K --> N[Answer]
L --> N
M --> N
Token-space (which words survive). The text fans out to two compressor families at once — LLMLingua-2 (+ LongLLMLingua's query-aware reranker/perplexity arm) and AttentionRAG (attention-guided sentence pruning). Both produce per-token keep decisions, which Winnow merges token-by-token over the original text under a user-picked boolean rule: intersection (keep only if both kept — aggressive) or union (keep if either kept — recall-safe). The merge aligns both methods to one canonical token sequence rather than doing string membership, and falls back to LLMLingua-only if AttentionRAG gates everything out.
Model-space (how it's answered). The compressed prompt then goes either to a
black-box API (Claude / GPT, provider-agnostic), or to our self-hosted
workers: Qwen + TurboQuant (KV-cache quantized to ~4 bits, 3–4× smaller via a
data-free random-rotation DynamicCache), or LCLM + TurboQuant — LCLM
compresses the context into a few latent soft tokens (fewer KV entries) while
TurboQuant compresses the bits per entry of the same decoder cache, so the savings
multiply.
Papers: LLMLingua-2 · LongLLMLingua · AttentionRAG (arXiv:2503.10720) · LCLM / End-to-End Context Compression at Scale · TurboQuant (arXiv:2504.19874). We also explored CompactPrompt's style-metric approach but it shipped no public dataset to reproduce against. A full project write-up lives in
DEVPOST.md.
Architecture
┌──────────────────────────┐
│ Browser (Next.js 15) │
│ │
┌──────────────┤ • Compare tab │
│ getUserMedia │ • Learn tab (voice) │
│ │ • Zustand store │
│ └────────┬──────────┬──────┘
│ │ │
│ WebSocket │ /api/ │ /api/project-chat
│ (opus chunks) │ compress │ /api/project-action
▼ ▼ ▼ /api/qa
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Deepgram │ │ FastAPI │ │ Anthropic │
│ Nova-3 │ │ (server.py) │ │ Claude │
│ streaming │ │ │ │ (streaming) │
└──────────────┘ └──────┬───────┘ └──────────────┘
│ modal RPC
▼
┌──────────────┐
│ Modal │
│ (T4 GPU) │
│ + LLMLingua │
│ -2 XLM │
└──────────────┘
- The browser opens its own WebSocket to Deepgram (auth via
/api/deepgram-token) and pushes Opus-encoded audio chunks. - Each
speech_finalutterance fires a request to/api/compress, which proxies to the local FastAPI. - FastAPI calls the Modal worker over Modal's RPC. The worker hosts LLMLingua-2 on a T4, with memory + GPU snapshots so cold starts are ~1 second.
- Q&A and Project chat go through Next.js API routes directly to Anthropic (Claude Sonnet 4.6 by default; pickable in the UI).
Tech stack
Frontend (web/)
- TypeScript, Next.js 15 (App Router), React 18
- Tailwind CSS, Framer Motion, Radix UI primitives (Slider, Switch, Slot)
- Zustand for state,
gpt-tokenizerfor client-side token counting fallback lucide-react,canvas-confetti,clsx + tailwind-merge
Speech
- Deepgram Nova-3 streaming (interim results,
speech_final, VAD, diarization, multilingual)
Compression
- LLMLingua-2 (
microsoft/llmlingua-2-xlm-roberta-large-meetingbank) - Hosted on Modal — T4 GPU, persistent HF cache volume, memory + GPU snapshots for ~1s cold start
- FastAPI / Pydantic / Uvicorn as a thin local proxy
LLM
- Anthropic Claude Sonnet 4.6 (default), Opus 4.7, Haiku 4.5; cost math also covers GPT-4o
- Streaming via SSE; prompt caching with
cache_control: ephemeralon the sources block
Local setup
Prerequisites:
- Python 3.11+
- Node.js 18+
- A Modal account (free tier works)
- A Deepgram API key
- An Anthropic API key
1. Set up the Python backend
# from repo root
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install modal fastapi "uvicorn[standard]" pydantic
2. Authenticate Modal and deploy the GPU worker
.venv/bin/modal setup # one-time, opens a browser
.venv/bin/modal deploy llmlingua2_modal.py # deploys the Compressor class
The first deploy takes a few minutes (image build + model download into the persistent volume). Subsequent deploys are near-instant.
3. Set up the Next.js frontend
cd web
npm install
cp .env.example .env
Fill in .env:
DEEPGRAM_API_KEY=...
ANTHROPIC_API_KEY=...
COMPRESS_BACKEND_URL=http://localhost:8000 # default, leave as-is
4. Run everything
In one terminal — the GPU worker proxy:
# from repo root
./run.sh # deploys (if needed) + warms + serves FastAPI on :8000
Honors SKIP_DEPLOY=1 and SKIP_WARMUP=1 for faster restarts during dev.
In another terminal — the frontend:
cd web
npm run dev # http://localhost:3000
Open http://localhost:3000.
Using the demo
- Click Start in the top right. The browser asks for mic permission.
- Talk. Watch the raw column fill on the left, compressed on the right with strike-throughs on dropped words. Stats bar updates live.
- Hit one of the preset probes under the Q&A box (or type a question) — answers fire against raw and compressed in parallel. A green "answers match" badge is the proof.
- Switch to Learn. The compressed transcript is auto-pinned as a source.
- Tap the big neon mic. Talk to Claude about what was said. Each pause auto-sends. Tap any Insight card to generate flashcards, action items, glossary, etc. — results stay cached.
Stage-safety toggles
- Live mic ⇄ Recorded at the top — instant swap. Recorded plays
web/public/fixtures/demo-transcript.jsonwith original timing. - Rec fixture / Save — capture a fresh recorded fallback from your current live session and download as JSON. Replace
public/fixtures/demo-transcript.jsonwith it so the fallback is literally you. - Director button or
?key — pulls up the keyboard cheat sheet.
Troubleshooting
Token mint failed: 500 / 403 FORBIDDEN
Your Deepgram API key doesn't have project-admin scope, so /v1/auth/grant rejects it. Winnow's /api/deepgram-token falls back to shipping the raw key to the browser (fine for local). Make sure DEEPGRAM_API_KEY is set in web/.env. Restart npm run dev after editing.
Chat error: invalid x-api-key / authentication_error
ANTHROPIC_API_KEY is missing, malformed, or stale. Edit web/.env, then restart npm run dev — Next only reads env vars at boot.
backend unreachable on compress
FastAPI isn't running. Start it: ./run.sh from the repo root.
Modal call fails / cold start is slow
First call after a deploy or a long idle period builds the GPU snapshot — that's ~30s once, then restores in ~1s. ./run.sh automatically warms the worker after deploy.
Port 3000 already in use
A previous dev server is still up. lsof -i :3000 to find the PID, then kill <pid>. Or use npm run dev -- -p 3001.
Mic not working in the browser Browser permission was denied. Chrome / Edge: click the lock icon next to the URL → Site settings → Microphone → Allow.
Repo layout
winnow/
├── llmlingua2_modal.py Modal app: LLMLingua-2 on a T4
├── server.py FastAPI proxy: HTTP → Modal RPC
├── warmup.py One-shot warm of the deployed worker
├── run.sh Deploy + warm + serve, one command
├── web/ Next.js frontend
│ ├── app/
│ │ ├── page.tsx Tab host + global controls
│ │ └── api/ compress / deepgram-token / qa / qa-stream / project-chat / project-action
│ ├── components/
│ │ ├── CompareView.tsx Raw vs compressed two-column view
│ │ ├── LearnView.tsx Claude-Projects-style workspace
│ │ ├── learn/ Sources, voice chat, insights
│ │ └── ... StatsBar, SourceToggle, QABox, RateSlider, etc.
│ ├── lib/
│ │ ├── sources/ TranscriptSource interface + live-mic + recorded impls
│ │ ├── pipeline.ts Wires source → /api/compress → store
│ │ ├── store.ts Zustand store, totals selector
│ │ └── tokens.ts Per-model pricing, filler stripper, cost math
│ └── public/fixtures/ Recorded fallback transcript
└── README.md ← you are here
License
Built for a hackathon. All third-party services and models retain their own licenses.
Analysis
View
Metric
- 16
- 11
- 4
- 3
Figures cover GitHub contributors during the hackathon window. A co-authored commit counts in full for each author, so per-member totals add up to more than the whole-team figures.
Technology
- AnthropicIn code
- CSSIn code
- HTMLIn code
- Hugging FaceIn code
- Next.jsIn code
- PythonIn code
- PyTorchIn code
- ReactIn code
- Tailwind CSSIn code
- TypeScriptIn code
- FastAPIClaimed
- VercelClaimed
10 of 12 appear in the indexed code. 2 claimed on Devpost could not be matched to code, which may simply mean the tool leaves no trace in the repository.
AI coding agents
- Claude CodeCommits
Detected from committed agent config files and commit authorship. Absence of a signal is not proof an agent was unused.
Codebase size
Source size
525 KB
Source files
82
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
TC960/winnow
104 files · 2.4 MB · @ 10dfab3
Structure
Interface
19 files · 18%Screens, components and styles rendered to the user.
API & routing
6 files · 6%Request entry points: routes, handlers and controllers.
Application logic
53 files · 51%Domain rules, services and shared utilities.
+2 more
Supporting
Layers are inferred from where files sit in the tree, not from reading the code. A project that names its directories unconventionally will read oddly here — open the file browser to check anything the diagram implies.
Languages
- Python60%
- TypeScript22%
- Markdown10%
- HTML8%
- Shell0%
- CSS0%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
web/package.json
npm · 23- @anthropic-ai/sdk
- @radix-ui/react-slider
- @radix-ui/react-slot
- @radix-ui/react-switch
- canvas-confetti
- clsx
- framer-motion
- gpt-tokenizer
- lucide-react
- next
- react
- react-dom
- tailwind-merge
- unpdf
- zustand
- +8 more
attentionrag/requirements.txt
pypi · 8- accelerate
- hf_transfer
- huggingface_hub
- modal
- protobuf
- sentencepiece
- torch
- transformers
Declared in the repository’s manifests at the indexed commit. A declared package is not proof it is used, and runtime dependencies are listed first.
This project’s features have not been analysed yet.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.