// 2026-04-19 · 6 min

Voice Inbox — głosowy asystent do Linear, którego zrobiłem sobie w parę godzin

Voice Inbox — głosowy asystent do Linear, którego zrobiłem sobie w parę godzin

TL;DR

Voice Inbox to lokalny daemon na Maca, który poluje Linear (+ gotowe pod Slack) i czyta mi głosem nowe zadania, komentarze, zmiany statusu. Raz na godzinę Haiku streszcza mi ostatnią godzinę w pięciu-sześciu punktach. Issues z priority Urgent/High dostają wyraźniejszy ton — intonacja, nie inny głos.

Repo: github.com/stroniarz/voice-inbox Licencja: Apache 2.0 (do dodania)

Problem

Prowadzę kilkanaście projektów naraz. Linear, Slack, Gmail, czasem SMS — informacje rozpierzchają uwagę. Przez ostatnie miesiące złapałem się na tym, że co pięć minut przełączam się między kartami, żeby sprawdzić czy nic nie wpadło. Efekt: rozpraszam się, tracę flow, a i tak 80% powiadomień to nic ważnego.

Chciałem czegoś przeciwnego — żeby informacja sama do mnie przychodziła, głosowo, w tle, ale tylko ta istotna. Żebym mógł dalej robić swoje i wiedzieć, co się dzieje bez otwierania czegokolwiek.

Rozwiązanie

Python daemon na Macu, który:

  1. Co minutę pyta Linear API o nowe zdarzenia (issues, komentarze, zmiany statusu)
  2. Archiwizuje wszystko w lokalnej SQLite (dedup + historia)
  3. Live — natychmiast czyta głosem krótki komunikat („Linear, nowe zadanie: header sklepu”) bez kosztu LLM
  4. Digest — co godzinę Haiku streszcza ostatnie 60 minut do max 6 zdań, naturalnym ciągłym tekstem („Raport z ostatniej godziny. Po pierwsze…, dalej…, na koniec…“)
  5. Issues z priority Urgent/High czyta tym samym głosem, ale z bardziej ekspresyjną intonacją (ElevenLabs stability: 0.3) i prefixem „Uwaga, pilne!”

Kolejka wypowiedzi jest jednokanałowa — nic się nie nakłada.

Co ciekawego pod spodem

Abstrakcja LLM — provider-agnostic

Na początku wpisałem na sztywno Anthropic. Po godzinie pomyślałem, że to głupie — mam klucze do kilku innych providerów, a na OpenRouter mam dostęp do wszystkiego za jeden klucz. Zrobiłem więc protokół LLMClient:

class LLMClient(Protocol):     def chat(self, system: str, user: str, max_tokens: int = 600) -> str: ...

I dwa adaptery: natywny Anthropic SDK oraz OpenAI-compatible (pokrywa OpenAI, OpenRouter, DeepSeek, Ollama — to samo API, inne base_url).

Config:

llm:   provider: anthropic   # anthropic | openai | openrouter | deepseek | ollama   model: claude-haiku-4-5-20251001   api_key_env: ANTHROPIC_API_KEY

Przełączenie na lokalną Gemma przez Ollama — jedna linia:

llm: {provider: ollama, model: gemma3:27b}

I już mam darmowe, offline LLM do streszczeń.

Abstrakcja TTS — intonacja zamiast drugiego głosu

Początkowo pilne eventy dostawały inny głos (męski Josh zamiast domyślnej Sary). Po dwóch dniach mnie to zaczęło wkurzać — za dużo „aktorów”. Zmieniłem na jeden głos, dwa profile:

tts:   default:     voice_id: EXAVITQu4vr4xnSDxMaL   # Sarah     stability: 0.5                    # neutralnie     speed: 1.2   critical:     voice_id: EXAVITQu4vr4xnSDxMaL   # ta sama Sarah     stability: 0.3                    # bardziej ekspresyjnie     speed: 1.2

Plus prefix tekstowy: "Uwaga, pilne! {treść}". ElevenLabs parsuje wykrzykniki i słowa-wzmacniacze, więc intonacja faktycznie się zmienia. Mózg rozpoznaje „pilne” w ułamku sekundy bez przestawiania się na inny głos.

i18n od razu, nie na koniec

Popełniłem błąd — najpierw wpisałem polskie teksty na sztywno w kod (prompty, templates short form). Po dwóch godzinach chciałem przejść na angielski, żeby sprawdzić jak brzmi. Przepisanie zajęło 30 minut. Po 30 minutach user mnie złapał: „Wybór języka musi być opcją, nie kasowaniem pliku”. Miał rację.

Zrobiłem i18n.py z prostym słownikiem per-locale:

MESSAGES = {     "pl": {         "linear_new_task": "nowe zadanie: {title}",         "urgent_prefix": "Uwaga, pilne! ",         "digest_system": "...pełny prompt po polsku...",     },     "en": { ... }, }

language: pl w configu. Jedna linia zmienia wszystko — short forms, prefixy, prompt do digestu. Nowy język = dodanie wpisu do słownika.

SQLite jako persistance layer

Trzy tabele:

  • seen — deduplikacja (source + external_id + timestamp)
  • cursor — ostatni timestamp pollowania per-source
  • events — archiwum eventów z treścią, do digestu

Żadnego ORM. Sześć funkcji. Cały persistence to 80 linii Pythona.

Koszty

Bo to pewnie ciekawe — a da się zrobić kilka wariantów, od „zero złotówek” po „premium”.

Wolumen przy moim użyciu (30-50 eventów dziennie, digest co godzinę = 24/dzień):

  • Live — ~50 wypowiedzi × ~75 znaków = ~110 tys. znaków/mies
  • Digest — 24 × ~600 znaków = ~430 tys. znaków/mies
  • Razem TTS — ~540 tys. znaków/mies
  • Haiku — 720 wywołań digestu, ~3-4M tokenów input miesięcznie

Wariant 1: zero złotówek (100% lokalnie)

llm:   provider: ollama   model: gemma3:27b      # lub mniejszy jeśli masz mało RAM tts:   provider: say   voice: Zosia           # macOS system voice, PL
  • LLM: Ollama z Gemma 3 (lub Llama 3, Qwen 2.5) — darmowe, lokalne, prywatne. Na MacBooku M1/M2/M3 z 16+ GB RAM działa płynnie.
  • TTS: macOS-owe say -v Zosia — 0 zł, brzmi jak syntezator z 2005 roku, ale czytelnie.
  • Linear: free plan (do 10 użytkowników, 250 issues).

Koszt: 0 zł. Dane nie wychodzą poza Twój Mac. Jakość streszczeń gorsza niż Haiku (Gemma streszcza ok, ale bez takiej finezji), głos syntetyczny.

Wariant 2: hybryda — tanio i ładnie (~$25/mies)

llm: {provider: anthropic, model: claude-haiku-4-5-20251001} tts:   live:    {provider: say, voice: Zosia}            # live — darmowo   digest:  {provider: elevenlabs, voice_id: ...}    # digest — ElevenLabs
  • Live przez say — krótkie komunikaty, brzmią jako tako i tak są krótkie
  • Digest przez ElevenLabs Creator ($22/100k znaków) — jakość tam, gdzie naprawdę warto (długie, spójne raporty)
  • Haiku do streszczeń — ~$3-15/mies

Koszt: ~$25/mies. Najlepszy stosunek jakość/cena moim zdaniem.

Wariant 3: premium (~$100/mies)

llm: {provider: anthropic, model: claude-haiku-4-5-20251001} tts:   default: {provider: elevenlabs, voice_id: ...}   critical: {provider: elevenlabs, voice_id: ..., stability: 0.3}

Wszystko przez ElevenLabs Pro ($99/500k znaków) — najwyższa jakość, priority routing przez voice_settings. To u mnie.

Koszt: ~$100-115/mies.

Na horyzoncie

  • Kokoro TTS (open source, self-host) — byłaby darmowa alternatywa dla say na English. Polski na razie nieobsługiwany, trzeba czekać.
  • Fish Audio — alternatywa dla ElevenLabs, pay-as-you-go, ~$15/1M UTF-8 bajtów. Dla polskiego wolumenu ~$5-8/mies.

Architektura

voice_inbox/ ├── adapters/       # polling (Linear GraphQL, Slack Web API) ├── llm/            # LLMClient + anthropic / openai_compat ├── tts/            # TTSClient + say / elevenlabs / openai + kolejka ├── i18n.py         # PL/EN teksty + prompty digestu ├── dedup.py        # SQLite archiwum + cursors ├── summarize.py    # generator digestu ├── config.py └── main.py         # orchestrator + digest worker (w osobnym wątku)

Każdy kawałek jest ~50-150 linii. Nic nie jest genialne, wszystko jest po prostu proste.

Status

MVP działający na Linear. W planach:

  • Slack adapter — kod gotowy, nieprzetestowany (trzeba założyć Slack App żeby dostać user tokena z odpowiednimi scope’ami)
  • Gmail adapter — będzie wymagał filtrowania („tylko od X, nie newslettery”)
  • SMS / iMessage — bezpośredni odczyt z ~/Library/Messages/chat.db
  • Menubar toggle — SwiftBar plugin do on/off + per-source
  • Grupowanie — 5 akcji na tym samym issue w 2 min = 1 wypowiedź („trzy komentarze na STR-165”)
  • Voice clone — własny głos przez ElevenLabs (dlaczego nie)

Dlaczego open source

Bo to jest rodzaj narzędzia, które albo sobie sam postawisz (wtedy chcesz czytelny kod i pełną kontrolę), albo nie zainstalujesz wcale. Nie ma sensu sprzedawać tego jako SaaS — użytkownik musi podać tokeny do Linear, Slack, Gmail, ElevenLabs. Lokalny daemon to jedyna rozsądna forma.

Jeśli komuś się to do czegoś przyda albo ktoś zrobi adapter do Jiry — świetnie.

Repo: github.com/stroniarz/voice-inbox

Jakby ktoś próbował u siebie i się przyciął — issues lub do mnie na michal@stroniarz.pl.