# Sloth Recap: wake up to 2 lines instead of 200 group-chat messages (EmbeddingGemma, 100% in your browser)

> Source: <https://dev.to/asosa933/sloth-recap-wake-up-to-2-lines-instead-of-200-group-chat-messages-embeddinggemma-100-in-your-1kp7>
> Published: 2026-10-20 12:07:25+00:00

*This is a submission for the [Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady](https://dev.to/challenges/hacktoberfest-week3-2026-10-19)*

**Sloth Recap** is a small web app for one very lazy moment: waking up and finding 200 new messages in a group chat.

You drop in the chat export (WhatsApp "Export chat" `.txt` or `.zip`, or just paste the text), type your name once, and press **🦥 Catch me up**. You get:

Group chats never sleep. In the morning you scroll through "lol", "😂", "ok" and three conversations happening at the same time, just to find the one question somebody asked *you*. Sloth Recap does that scrolling for you, so whoever reads the group last in the morning gets to be the lazy one.

One design rule I care about: **Sloth Recap only quotes real messages.** It picks them, it never writes new text, so it can't invent a plan nobody made. When the recap says "Technician comes Wednesday between 9 and 12", someone really wrote that.

👉 **Live demo:** [https://asosa933.github.io/sloth-recap/](https://asosa933.github.io/sloth-recap/)

Click one of the two sample chats (🏠 English flatmates or ⚽ Italian football group), then **Catch me up**. The first AI run downloads the model once (about 175 MB with WebGPU or about 310 MB on CPU) and then the browser keeps it cached. If you don't want the download, untick "Use local AI" and you get the simple keyword mode instantly.

The result for the flatmates chat: 38 new messages became 2 lines, plus the 3 messages that actually need me.

It also works in Italian (and on mixed-language chats), because the model is multilingual:

**Wake up to 2 lines, not 200 group-chat messages.**

Sloth Recap reads a group chat export (WhatsApp `.txt` or `.zip`, or pasted text) and gives you:

Everything runs **in your browser**. The open-weight model [EmbeddingGemma](https://ai.google.dev/gemma/docs/embeddinggemma) runs locally with [transformers.js](https://github.com/huggingface/transformers.js). There is no server, no account and no API key, and your chat is never uploaded.

👉 **Live demo:** [https://asosa933.github.io/sloth-recap/](https://asosa933.github.io/sloth-recap/) (click a sample chat, then "Catch me up")

Built for the DEV [Hacktoberfest Open-Source AI Challenge, Week 3: "Slow and Steady"](https://dev.to/challenges/hacktoberfest-week3-2026-10-19). The project was created during the Week 3 window (October 2026).

On the built-in "Flat 3B" sample, 38 messages arrived…

It's plain HTML, CSS and JavaScript: no build step, no backend, hosted for free on GitHub Pages. The pipeline looks like this:

```
chat export → parser → "messages after my last one" → drop "ok/lol/👍"
            → EmbeddingGemma vectors (Web Worker) → pick key messages → group the rest
            → 2 lines + "needs you" + topics
```

**The open-weight model: EmbeddingGemma.** In a Web Worker I load [`onnx-community/embeddinggemma-300m-ONNX`](https://huggingface.co/onnx-community/embeddinggemma-300m-ONNX) with [transformers.js](https://github.com/huggingface/transformers.js). It runs on WebGPU when the browser has it and on the CPU (WASM) otherwise. Every new message becomes a vector, using EmbeddingGemma's `task: sentence similarity` prompt. Then:

The **🙋 Needs you** list is intentionally *not* AI. It's simple rules: your name as a whole word or `@name`, plus question patterns in English and Italian. For "did someone ask me something?" I want rules that are 100% predictable.

**Deterministic fallback.** If the model can't load (or you turn it off), the exact same pipeline runs on TF-IDF keyword vectors. I tested this by blocking Hugging Face in headless Chrome: the app showed "Local AI: failed to load" and still produced the recap in simple mode.

**Making it fast on a normal laptop CPU** was the part I learned the most from. On my CPU-only test machine (headless Chrome, 38 messages):

The 4-bit export is the smaller download, but on WASM it was about 3x slower than the 8-bit file. I don't know exactly which operator is the slow one, but the measurement was clear, so the app uses 8-bit on CPU (and 4-bit only on WebGPU). Multi-threading needs a "cross-origin isolated" page, and GitHub Pages can't set those headers. So I added a tiny service worker (`sw.js`, about 20 lines) that adds them to the site's own files. After the first load, the model can use several CPU threads (4 on my test machine).

**Testing.**

`npm test`: 11 unit tests with `node:test` (WhatsApp Android/iPhone formats, US/EU dates, multi-line messages, fillers, "needs you", the fallback digest, reading a `.zip`).` eval/eval-model.mjs` runs the real model in Node on 5 small chats I wrote and labelled by hand (the 2 demo chats plus 3 held-out ones, one of them mixed Italian/English):
|  | EmbeddingGemma | TF-IDF fallback | 
|---|---|---|
| nearest message is about the same topic | 67/88 (76%) | 64/88 (73%) | 
| real topics covered by the top 3 lines | 14/15 | 14/15 | 
| messages grouped under the right line | 72/81 (89%) | 64/78 (82%) | 
| mixed-language chat, grouped right | 11/13 | 9/13 | 

To be honest: it's a tiny dataset that I wrote myself, so it's a sanity check, not a benchmark. Both modes pick good lines on these chats. The model's advantage shows when it **groups** messages, especially short replies and messages that switch language ("The heating in my room is not working" / "Anche da me il termosifone è freddo").

**AI disclosure:** I'm a beginner from Italy and I built this with a lot of help from an AI coding assistant (code, tests, debugging, measurements). The idea, the "never write new text" rule, the choices about what is AI and what is rules, and the testing are mine. I made sure I understand every file.

For this project, open weights are the whole point:

`src/model-worker.js`. If a better small open embedding model comes out next month, I can try it in an afternoon, and the TF-IDF fallback means the app never depends on any single model.
Measured by the app itself on the built-in flatmates chat:

A busier group means more reading time, but the recap is still 2 lines. 🦥

Thanks for reading! If your group chat export breaks the parser (there are *many* date formats out there), please open an issue with a fake example and I'll fix it. 🦥
