Voice Capture
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
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.
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.
recordings:read — čtení vlastních nahrávek (výchozí)recordings:write — navíc zakládání nahrávek (Zkratky)Klíč nikdy nesmí: vytvořit další klíč, smazat účet, stáhnout zvuk.
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.
Dvě akce „Get contents of URL" za sebou. Klíč musí mít rozsah zápisu (v aplikaci zaškrtni pro Zkratky).
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://…" }
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.
| 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 |
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).
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í).
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í
curl -s -H "Authorization: Bearer $VOICE_CAPTURE_KEY" \
"$BASE/v2_get_recording?limit=1" | jq -r '.items[0].transcript.text'
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)'
Nahrávka nemusí mít přepis hned. status projde AWAITING_TRANSCRIPT →
TRANSCRIBING → READY. 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.