Voice Capture

API — čti si vlastní přepisy

Osobním klíčem si hotové přepisy a AI výstupy vytáhne Notion, Zapier, vlastní skript nebo tvoje aplikace. Klíč umí jen číst, patří jednomu účtu a kdykoli ho zrušíš.

Databáze i zpracování běží v EU (Frankfurt / Nizozemsko). Základ všech adres:

https://europe-west3-voice-recorder-2026.cloudfunctions.net

Rychlý start

  1. V aplikaci: Nastavení → Propojení s externími aplikacemi → Vytvořit klíč.
  2. Tamtéž Zkopírovat hotovou URL — dostaneš adresu i s klíčem.
  3. Zavolej ji:
curl "https://europe-west3-voice-recorder-2026.cloudfunctions.net/v2_get_recording?limit=5&key=vcr_…"

Hotová URL se chová jako heslo: kdo ji má, čte tvoje přepisy. Do skriptu ji dávej přes proměnnou prostředí, ne natvrdo do kódu.

Klíč umí jen číst (pokud mu nedáš rozsah zápisu): nahrávat, mazat ani sahat na účet s ním nejde. Zrušíš ho tamtéž a do minuty přestane platit.


Autorizace

Dvě cesty, každá k něčemu jinému:

k čemu jak
Firebase ID token aplikace a web Authorization: Bearer <id_token>
Osobní klíč vcr_… Notion, Zapier, Zkratky, vlastní skripty Authorization: Bearer vcr_… nebo ?key=vcr_…

Klíč vytvoříš v aplikaci: Nastavení → Propojení s externími aplikacemi. Plný klíč uvidíš jen jednou — server si dál pamatuje jen jeho otisk.

Statický token z verze 1.1.1 tu neplatí. Byl to master klíč ke všem datům všech uživatelů; osobní klíč patří jednomu účtu, má rozsah a jde zrušit.

Rozsahy

Klíč nikdy nesmí: vytvořit další klíč, smazat účet, stáhnout zvuk.


Čtení nahrávek

GET /v2_get_recording?limit=20&key=vcr_…
GET /v2_get_recording?id=<recording_id>&key=vcr_…

Parametry: limit 1–100 (výchozí 20) a dva kurzory podle toho, kam se posouváš: since=<ISO8601> vrací novější záznamy vzestupně (pro deníček: „co přibylo od minule"), before=<ISO8601> starší sestupně (donačítání historie). Zadat jde jen jeden; since má přednost.

Odpověď seznamu:

{
  "items": [{
    "id": "…", "title": "Kontrolní den Slatina",
    "status": "READY", "created_at": "…Z", "recorded_at": "…Z",
    "duration": 92.5, "device": "iPhone", "location": {…}, "bookmarks": [],
    "transcript": {
      "provider": "apple",           // nebo "gemini"
      "language": "cs-CZ",
      "text": "…",                   // doslovný přepis, neměnný
      "segments": [{"start": 0.0, "text": "…"}],
      "words": [{"s": 0.0, "t": "Tak"}]
    },
    "outputs": [{                    // AI výstupy podle šablon
      "template_id": "zapis-z-jednani", "template_name": "Zápis z jednání",
      "title": "…", "source_type": "transcript",
      "sections": { "summary": "…", "tasks": [{"text": "…", "owner": "…"}] }
    }]
  }],
  "count": 1,
  "next_since": "…Z",    // kurzor pro další čtení dopředu
  "next_before": "…Z"    // kurzor na starší stránku (chybí u dotazu se `since`)
}

Ven jde whitelist polí — interní cesty v úložišti, vlastník ani náklady v odpovědi nikdy nejsou.

sections mají tvar podle šablony: text, seznam řetězců, úkoly (text/owner/deadline), mluvčí (label/description), časová osa (time/event) nebo hodnoty (label/value/estimated). U hodnot estimated rozlišuje, co uživatel řekl, a co AI odhadla — v deníku zdravotních záznamů je to ten podstatný rozdíl.


Zkratky: nahrát záznam ze Zkratky

Dvě akce „Get contents of URL" za sebou. Klíč musí mít rozsah zápisu (v aplikaci zaškrtni pro Zkratky).

1. Založit nahrávku

POST /v2_create_recording
Authorization: Bearer vcr_…
Content-Type: application/json

{ "duration": 23.4,
  "filename": "zkratka.m4a",
  "title": "Ze Zkratky",              // volitelné
  "needs_transcription": true,        // přepis udělá AI
  "want_audio_upload": true }

Odpověď: { "recording_id": "…", "status": "AWAITING_TRANSCRIPT", "upload_url": "https://…" }

2. Nahrát zvuk

PUT <upload_url>
Content-Type: audio/m4a
<binární obsah souboru>

Bez hlavičky Authorization — autorizaci nese podpis v adrese, který platí 15 minut. Zbytek se stane sám: přepis udělá Gemini a podle nastavení může vzniknout i AI výstup.

Když už přepis máš (třeba z diktování), pošli ho místo needs_transcription:

{ "duration": 23.4,
  "transcript": { "text": "…", "language": "cs-CZ" } }

Nahrávka pak vzniká rovnou hotová a nic se nikam nepřepisuje.


Chyby

kód co znamená
401 chybí nebo neplatí token či klíč
403 klíč nemá potřebný rozsah (typicky zápis)
402 došly minuty ({"error": "NO_CREDIT", "kind": "audio"})
404 záznam neexistuje, nebo není tvůj — obojí vypadá stejně
409 chybí přepis nebo zvuk pro daný výstup

Zrušení klíče

V aplikaci, Nastavení → Propojení s externími aplikacemi → Zrušit klíč. Přestane platit do minuty (servery si ověřený klíč krátce drží v paměti).


Přepsat nahrávku, která přepis nedostala

POST /v2_reprocess_recording — jen s ID tokenem (ne s API klíčem).

{ "recording_id": "abc123" }

Pro nahrávku, která uvízla ve stavu NO_CREDIT (došly minuty) nebo FAILED. Server přepis sám neopakuje: čeká, až uživatel doplní kredit a řekne si o to. Endpoint jen nastaví dokument zpátky na AWAITING_TRANSCRIPT a přepíše objekt v úložišti sám na sebe — přepis pak proběhne obvyklou cestou přes storage trigger, takže se výsledek ničím neliší od prvního nahrání.

Odpovědi navíc: 409 ALREADY_TRANSCRIBED (nahrávka přepis má — originál je neměnný), 409 NOT_REPROCESSABLE (jiný stav než NO_CREDIT/FAILED), 409 NO_AUDIO (zvuk v úložišti není), 402 NO_CREDIT (minuty pořád chybí).


Příklady

Python — co přibylo od minule

Kurzor next_since si ulož a příště ho pošli zpátky; server vrátí jen novější záznamy, takže nic nepřečteš dvakrát.

import os, json, pathlib, requests

BASE = "https://europe-west3-voice-recorder-2026.cloudfunctions.net"
KEY = os.environ["VOICE_CAPTURE_KEY"]        # vcr_…
STATE = pathlib.Path("~/.voice-capture-cursor").expanduser()

params = {"limit": 50}
if STATE.exists():
    params["since"] = STATE.read_text().strip()

r = requests.get(f"{BASE}/v2_get_recording", params=params,
                 headers={"Authorization": f"Bearer {KEY}"}, timeout=30)
r.raise_for_status()
data = r.json()

for item in data["items"]:
    print(item["recorded_at"], "—", item.get("title") or "(bez názvu)")
    transcript = (item.get("transcript") or {}).get("text", "")
    if transcript:
        print(transcript[:200])

if data.get("next_since"):
    STATE.write_text(data["next_since"])       # až po úspěšném zpracování

Shell — poslední nahrávka jako čistý text

curl -s -H "Authorization: Bearer $VOICE_CAPTURE_KEY" \
  "$BASE/v2_get_recording?limit=1" | jq -r '.items[0].transcript.text'

Úkoly ze všech zápisů z jednání

sections mají tvar podle šablony — úkoly jsou pole objektů, ne řetězců.

curl -s -H "Authorization: Bearer $VOICE_CAPTURE_KEY" \
  "$BASE/v2_get_recording?limit=50" \
| jq -r '.items[].outputs[]?
         | select(.template_id == "zapis-z-jednani")
         | .sections.tasks[]?
         | "- \(.text)" + (if .owner then " (\(.owner))" else "" end)'

Na co si dát pozor

Nahrávka nemusí mít přepis hned. status projde AWAITING_TRANSCRIPTTRANSCRIBINGREADY. Filtr podle stavu server nemá — přefiltruj si odpověď sám podle pole status a nehotové si přečti při dalším průchodu.

Nahrávka může přibýt zpětně. Záznam pořízený offline se odešle, až je telefon online, takže se v seznamu objeví s dřívějším recorded_at. Proto kurzor jede podle created_at, ne podle času nahrávání — a proto se vyplatí deduplikovat podle id.

Prázdný přepis je platný výsledek. Když v nahrávce nebyla řeč, text je prázdný a no_speech je true. Není to chyba; dřív v takovém případě AI obsah vymyslela, dnes ho radši nevrátí.

Tvrdý rate limit zatím není, ale čti dávkově (limit až 100), ne v cyklu po jednom záznamu. Server si u klíče zapisuje čas posledního použití, takže je v aplikaci vidět, že něco čte — a až limit přibude, bude vycházet z běžného provozu.