// 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.