Sloth Recap 在瀏覽器把羣組聊天整理成兩行摘要
Sloth Recap: wake up to 2 lines instead of 200 group-chat messages (EmbeddingGemma, 100% in your browser)
作者 asosa933 製作 Sloth Recap,讓用戶在瀏覽器匯入羣組聊天紀錄後,取得兩行摘要、需回覆訊息及按主題整理的內容。它以 transformers.js 在瀏覽器本機運行 EmbeddingGemma,只挑選原有訊息、不生成新文字,聊天紀錄亦不會上傳伺服器。內置 Flat 3B 樣本的 38 則新訊息被整理成兩行摘要,另列出 3 則需回覆訊息。
This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
What I Built
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:
- ☕ a 2-line recap of what happened since your last message,
- 🙋 Needs you: only the messages that mention you or ask you something,
- 🧺 Topics: every other message tucked under the line it belongs to, in case you want the details.
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.
Demo
👉 Live demo: 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:
Code
asosa933
/
sloth-recap
Wake up to a 2-line recap of your group chat instead of 200 messages. 100% in-browser with open-weight EmbeddingGemma (transformers.js).
🦥 Sloth Recap
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:
- a 2-line recap of what happened since your last message,
- 🙋 Needs you: the messages that mention you or ask you something,
- 🧺 topics: every message grouped under the line it belongs to, if you want the details.
Everything runs in your browser. The open-weight model EmbeddingGemma runs locally with 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/ (click a sample chat, then "Catch me up")
Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 3: "Slow and Steady". The project was created during the Week 3 window (October 2026).
The lazy payoff
On the built-in "Flat 3B" sample, 38 messages arrived…
How I Built It
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 with 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:
- Center the vectors. To a small embedding model, all chat messages look "kind of similar". On my test chats, unrelated pairs averaged a cosine similarity of about 0.58 and same-topic pairs about 0.63, which is almost no gap. Subtracting the chat's average vector moved them to about -0.07 and +0.06, so the gap got more than twice as big and topics became much easier to tell apart.
- Borrow context from neighbours. "Work for me" or "Ma dai 😡" mean nothing alone, so each message gets a bit of the vectors of the messages around it.
- Pick key messages. A message scores high if many others talk about the same thing, if it has concrete facts (a time, a day, a price) and if it's recent. I pick them one by one and penalise messages too similar to ones already picked (the classic MMR trick), so the 2 lines are about 2 different things.
- Group everything else under the key message it is closest to. Topics where someone asked me something go first.
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):
- 4-bit "no_gather" model, 1 thread: about 26 s 😴
- 8-bit model, 1 thread: about 8 s
- 8-bit model, 4 threads: about 3 s
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 withnode:test(WhatsApp Android/iPhone formats, US/EU dates, multi-line messages, fillers, "needs you", the fallback digest, reading a.zip). - A Puppeteer script opens the live GitHub Pages site in headless Chrome. It downloads the model, runs both samples with the AI, checks the "needs you" counts and the copy button, loads a
.zipexport, and runs the no-AI mode. All the screenshots and the GIF in this post were captured from the live site. -
eval/eval-model.mjsruns 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.
Why Does Open Innovation Matter?
For this project, open weights are the whole point:
- Group chats are private. They're full of other people's messages, addresses, plans and jokes. Pasting your friends' messages into a cloud API without asking them isn't fair to them. With EmbeddingGemma running in the browser tab, the chat never leaves the device. There's no server that could even receive it.
- It costs nothing to run. No API key, no per-message price, no account. The model runs on your own CPU/GPU, and the site is a static page on GitHub Pages. A "lazy" tool can't come with a monthly bill.
- No AI service to depend on. After the first visit the model comes from the browser cache. I checked it: with Hugging Face completely blocked, the second visit still ran the AI. Nobody can rate-limit it, change its price, or shut it down.
- I could look inside and change things. Because the weights, the ONNX exports and the runtime are all open, I could compare the 4-bit and 8-bit files on my own machine, measure that one was 3x slower, and switch. A closed API is a black box: you get one speed and one price.
-
It's swappable. The model name is one line in
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.
The Lazy Payoff
Measured by the app itself on the built-in flatmates chat:
- 38 new messages arrived after my "going to bed" message.
- Reading them all: about 1 min 27 s (word count at 200 words per minute, before even counting the scrolling).
- Reading the recap and the "needs you" list: about 18 s, and I only have to answer 3 messages: open the door for the technician, bring the tiramisu, confirm Wednesday.
- On my CPU-only test machine the AI needed about 4 s to read those 38 messages (after the one-time model download, which took about 30 s in my tests).
A busier group means more reading time, but the recap is still 2 lines. 🦥
Prize Categories
- Best Use of Gemma: the AI in Sloth Recap is EmbeddingGemma, Google's open-weight Gemma embedding model. It runs locally in the browser with transformers.js (WebGPU or WASM) and powers the topic finding and grouping, with no server and no API.
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. 🦥
來源:DEV Community · dev.to




