Gebruik de HTTP-API van utt om audio uit een ander proces of apparaat te transcriberen. Bouw een extensie om lokale audio te versturen, transcripties te ontvangen, tekst te filteren of instellingen en statusinformatie aan het utt-venster toe te voegen. Beide interfaces werken zonder SDK.
Met de HTTP-API kan een telefoon, script of app audio naar utt sturen en een transcriptie ontvangen. utt draait het spraakmodel altijd op je Mac, ook als de opname van een ander apparaat komt.
Standaard uitgeschakeld. Zet de API aan via Settings, Connect, API.
De instelling Bereik bepaalt wie verbinding kan maken. utt bindt de listener aan de bijbehorende netwerkinterface en controleert het adres van elke aanvrager.
| Instelling | Bindt aan | Accepteert |
|---|---|---|
| This Mac only | 127.0.0.1 | Loopback. |
| This Mac and my local network | Alle interfaces | Loopback, link-local en elke peer op een subnet waar een van de interfaces van deze Mac zich op bevindt. |
| Anywhere | Alle interfaces | Alles wat de poort kan bereiken. |
Het 'lokale netwerk' wordt bepaald aan de hand van de eigen interfaces van de Mac, niet op basis van privé-adresbereiken. Een VPN-peer heeft een privéadres maar kan ver weg zijn. Een telefoon op dezelfde wifi heeft via IPv6 vaak een wereldwijd routeerbaar adres.
Er is geen TLS. Plaats de API achter een reverse proxy of VPN als het verkeer je eigen netwerk verlaat. Als de poort al bezet is, start de listener niet en meldt de instellingenkaart dat.
Elk request, inclusief /health, bevat een bearer-token:
Authorization: Bearer <token>
Het token wordt aangemaakt wanneer de API voor het eerst wordt ingeschakeld en staat op de instellingenkaart. New vervangt het token en sluit alle clients buiten die met het oude token zijn ingesteld. Met een leeg token start de server helemaal niet.
/docs accepteert ook ?token=…, omdat de adresbalk van een browser geen header kan instellen. Geen enkele route die audio verwerkt doet dat.
De basis-URL is http://<host>:<port>. Gebruik 127.0.0.1 voor This Mac only. Gebruik anders de Bonjour-naam van de Mac, zoals mac.local; die blijft hetzelfde als het IP-adres verandert.
GET /docs
De OpenAPI-referentie, weergegeven door Redoc en aangeboden door utt zelf. Daardoor beschrijft die altijd de build die antwoord geeft. De link API reference op de instellingenkaart opent de pagina met het token al in de URL.
GET /health
Geeft { "ok": true, "version": "2.0.0" } terug.
POST /transcribe
De body is het audiobestand zelf. Geen multipart, geen JSON-envelope, geen velden.
Content-Type kiest de reader: audio/wav (de standaard voor alles wat niet wordt herkend), audio/mp4, audio/mpeg, audio/aiff, audio/flac of audio/caf.X-Utt-Hints is een optionele, door komma's gescheiden lijst met namen of termen die je in de opname verwacht, zoals een repository, product of persoon. utt past een hint alleen toe als het herkende woord al sterk overeenkomt. Een gemiste correctie is veiliger dan het verkeerde woord vervangen.curl -X POST http://mac.local:8756/transcribe \
-H "Authorization: Bearer $UTT_TOKEN" \
-H "Content-Type: audio/wav" \
--data-binary @clip.wavJSON
{
"text": "Hello world, this is a test of the transcription API.",
"raw": "hello world this is a test of the transcription api",
"stages": ["formatting", "replacements"],
"timings": {"decode": 1802.5, "replacements": 0.4, "formatting": 0.2}
}De transcriptie heeft dezelfde pipeline doorlopen als bij de sneltoets: de engine en het model uit Settings, daarna de woordvervangingen, eventueel de opschoningsstap en de opmaakregels. Een client krijgt precies wat utt anders zou hebben geplakt.
text is die transcriptie en verandert niet van vorm, zodat een client die voor het oudere antwoord met één veld is geschreven blijft werken. Daarnaast:
raw: wat de herkenner hoorde, vóór elke stap en vóór je eigen hints. Gelijk aan text als niets de tekst heeft veranderd.stages: welke stappen de woorden daadwerkelijk hebben veranderd: replacements, cleanup, formatting, filter (een door de gebruiker geïnstalleerde extensie) en hints (van jou). Een lege waarde betekent dat je precies kreeg wat er werd gehoord. Hints staan als aparte stap vermeld en zijn niet in raw verwerkt, omdat raw het enige blijft wat een client niet kan reconstrueren.timings: hoeveel tijd elk onderdeel kostte, in milliseconden, plus decode, de herkenner, waar de meeste wachttijd in zit. Een uitgevoerde stap die de woorden niet veranderde staat hier wel, maar niet in stages; die duurde hoe dan ook even lang. Samen vormen ze niet je volledige roundtrip.cleanupSkipped: alleen aanwezig als de gebruiker opschonen heeft ingeschakeld en die stap niet op deze opname is uitgevoerd: unavailable, guardrail, timeout, tooLong, tooShort of failedVerification. De transcriptie is alsnog afgeleverd.Fouten hebben de vorm { "error": "…" } met een status:
400 | Geen audio of een lege body. |
401 | Ontbrekend of onjuist token. |
404 | Pad bestaat niet. |
413 | Body groter dan 25 MB. |
500 | De engine is mislukt. |
Eén request per verbinding. Het antwoord bevat Connection: close; er is geen keep-alive.
De API-instellingen bevatten een knop Copy guide. Die kopieert de basis-URL van deze Mac, het wireformat, de audio-instellingen en een werkende URLSession-client. De briefing waarschuwt ook voor veelgemaakte fouten. Het token blijft een placeholder, zodat je het geheim niet in een chat plakt.
Een extensie verbindt een programma op je Mac met utt. Het programma kan een daemon, menubalk-app of script zijn. Voeg een manifest toe en utt bouwt de instellingenpagina. Daarna schrijft utt de keuzes van de gebruiker naar een bestand dat je programma kan lezen.
Extensies hoeven zich niet bij utt te registreren en roepen geen aparte API aan. Beide programma's lezen en schrijven bestanden in de extensiemap. Elk programma kan opnieuw starten zonder de verbinding te verliezen.
~/Library/Application Support/dev.jurrejan.utt/extensions/
Vóór utt 2.0 heette deze map plugins/. utt verplaatst de oude map één keer. Programma's die naar plugins/ blijven schrijven, bereiken utt niet meer.
Elke mogelijkheid heeft een eigen sleutel in het manifest. Vraag alleen om de mogelijkheden die je programma gebruikt.
| Wat je programma doet | Declareren of schrijven | Wat utt levert |
|---|---|---|
| Stuurt lokale audio voor transcriptie | sendsAudio | Een jobs-map voor opnames en antwoorden. |
| Ontvangt elke voltooide transcriptie | wantsTranscripts | De nieuwste definitieve transcriptie in één bestand. |
| Volgt woorden terwijl iemand praat | wantsPartials | Een stroom voorlopige tekst via één bestand. |
| Verandert tekst voordat die wordt afgeleverd | filtersTranscripts | Een request-en-reply-uitwisseling van twee seconden. |
| Voegt configuratie en commando's toe | settings en actions | Bediening op de extensiepagina en in het optionele menu. |
| Toont of het proces gezond is | daemon en status.json | Live status, een herstartactie en een optioneel menu. |
| Roept de HTTP-API aan | needsApi | Het token en de poort in het waardenbestand zolang de API aanstaat. |
Gebruik skipsTextStages wanneer je extensie een letterlijke transcriptie nodig heeft. De instelling geldt alleen voor audio die door die extensie is verstuurd. De eigen dicteeropnames van de gebruiker veranderen nooit.
| Bestand | Geschreven door | Bevat |
|---|---|---|
<id>.json | Jij | Het manifest: naam, icoon en weer te geven instellingen. |
<id>.consent.json | utt | De goedkeuring van de gebruiker, uitgeschakelde status en wachtrijprioriteit. |
<id>.values.json | utt | Wat de gebruiker heeft gekozen, elke sleutel, elke keer. |
<id>.jobs/ | Beide | Opnames die jij plaatst en transcripties die utt ernaast zet. |
<id>.transcript.json | utt | De nieuwste transcriptie, als je daarom hebt gevraagd. |
<id>.partial.json | utt | De voorlopige woorden terwijl iemand praat. |
<id>.filter/ | Beide | Voltooide transcripties die je vóór aflevering mag aanpassen. |
<id>.action.json | utt | De nieuwste druk op een knop op je pagina of in je menu. |
<id>.status.json | Jij | Enkele regels die bovenaan je pagina alleen-lezen worden getoond. |
Schrijf <id>.json telkens wanneer je programma start. Door het bestand opnieuw te schrijven herstel je het manifest na een deïnstallatie, een reset van de instellingen of een verwijderde extensiemap.
deckhand.json
{
"id": "deckhand",
"name": "Deckhand",
"blurb": "One sentence under your name: what the extension does for the person.",
"description": "A short paragraph for the About section of your page.",
"website": "https://example.com/deckhand",
"repository": "https://github.com/example/deckhand",
"systemImage": "sailboat",
"needsApi": false,
"wantsTranscripts": false,
"sendsAudio": false,
"wantsPartials": false,
"filtersTranscripts": false,
"skipsTextStages": [],
"tint": "#3EAFB4",
"showsInMenuBar": true,
"daemon": {"label": "com.example.mydaemon"},
"actions": [
{"key": "stop", "label": "Stop", "detail": "What it does.", "confirms": true},
{"key": "openLog", "label": "Open log"}
],
"settings": [
{"key": "route", "kind": "choice", "label": "Route",
"options": ["auto", "keyboard"], "value": "auto",
"detail": "Explains the setting, shown under the label."},
{"key": "deliver", "kind": "bool", "label": "Send into sessions", "value": true},
{"key": "prefix", "kind": "string", "label": "Prefix", "value": ""},
{"key": "delay", "kind": "number", "label": "Delay", "value": 0}
]
}id moet gelijk zijn aan de bestandsnaam zonder .json. Alleen kleine letters, cijfers, ., _ en -. Een afwijking wordt genegeerd.kind is bool, string, number of choice. value is de standaardwaarde en moet overeenkomen met het type. Een keuze heeft options nodig en een standaardwaarde die daartussen staat.systemImage is een SF Symbol. Een onbekend symbool wordt weggelaten.id en name zijn verplicht. Weggelaten sleutels krijgen hun standaardwaarde; het voorbeeld hierboven toont ze allemaal.Je installeert een extensie door het manifest in een map te plaatsen. Het manifest is niet ondertekend en wordt door geen installer gecontroleerd. Daarom blijft elke nieuwe extensie in afwachting totdat de gebruiker die goedkeurt via Settings, Connect, Extensions.
Zolang de goedkeuring nog niet is gegeven, maakt utt geen waardenbestand aan, transcribeert het geen opnames, deelt het geen transcripties en stelt het het API-token niet beschikbaar. Je programma kan zichzelf niet goedkeuren. Het moet veilig blijven werken als goedkeuring nooit komt. Stuurt het vóór goedkeuring audio in, dan geeft utt een fout terug:
{"error": "utt is waiting for you to approve this extension. …"}
Behandel een ontbrekend waardenbestand hetzelfde: de gebruiker heeft je pagina nog niet geopend of nog geen toestemming gegeven. Gebruik tot het verschijnt de standaardwaarden uit je eigen manifest.
Bij goedkeuring plaatst de gebruiker je ook in de wachtrij als je audio verstuurt: First, In turn of Last. De instelling bepaalt welke extensie als volgende aan de beurt is wanneer meerdere extensies werk in de wachtrij hebben. De huidige transcriptie wordt niet onderbroken en de eigen opnames van elke extensie blijven op volgorde.
De beslissing staat in <id>.consent.json, dat utt schrijft en dat jij wel mag lezen maar nooit schrijven. Een door jou geplaatst bestand wordt overschreven in plaats van opgevolgd. Een extensie die zichzelf kan goedkeuren zou precies het beveiligingsgat zijn dat dit voorkomt:
{"decision": "approved", "decidedAt": "2026-09-10T14:22:07Z", "priority": "normal"}
Helemaal geen bestand betekent wachten, net als een ontbrekend waardenbestand. "disabled" betekent dat de gebruiker je eerder heeft goedgekeurd en sindsdien via je pagina heeft uitgeschakeld. De extensie doet dan niets en het bestand vermeldt die status. priority is de term die het bestand voor de bovenstaande wachtrij gebruikt: "next", "normal" of "last".
utt schrijft <id>.values.json wanneer de gebruiker iets wijzigt en één keer nadat je het manifest hebt geïnstalleerd, zodat het bestand al bestaat voordat iemand de pagina opent.
deckhand.values.json
{
"revision": 4,
"values": {"route": "auto", "deliver": true, "prefix": "", "delay": 0},
"api": {"token": "…", "port": 8756}
}values-object; een ontbrekende sleutel betekent nooit 'ongewijzigd'.revision gaat bij elke schrijfactie één omhoog. Vergelijk het met de laatste waarde die je zag, niet met de wijzigingstijd.api verschijnt alleen als je manifest needsApi instelt, de gebruiker je heeft goedgekeurd en de API aanstaat. Afwezigheid betekent 'nu niet beschikbaar'. Lees nooit het eigen instellingenbestand van utt.Poll het bestand en behandel een ontbrekend bestand als 'de gebruiker heeft de pagina nog niet geopend':
struct Settings: Decodable { var route: String; var deliver: Bool; var prefix: String; var delay: Double }
struct Values: Decodable { var revision: Int; var values: Settings }
let file = URL.homeDirectory.appending(path: "Library/Application Support/dev.jurrejan.utt/extensions/deckhand.values.json")
var seen = 0
while true {
if let data = try? Data(contentsOf: file),
let doc = try? JSONDecoder().decode(Values.self, from: data),
doc.revision != seen {
seen = doc.revision
apply(doc.values) // every key, every time
}
try await Task.sleep(for: .seconds(1))
}Stel "sendsAudio": true in en utt maakt <id>.jobs/ aan. De bestandsuitwisseling blijft op de Mac en werkt ook als de HTTP-API uitstaat. Gebruik voor lokale audio de jobs-map in plaats van een netwerkverbinding te openen.
clip-1.wav.part, en hernoem die daarna naar clip-1.wav. utt pakt alleen audio-extensies op, dus een halfgeschreven bestand blijft onzichtbaar totdat het door hernoemen compleet wordt.clip-1.json ernaast. Precies één van text en error is aanwezig.let jobs = URL.homeDirectory.appending(path: "Library/Application Support/dev.jurrejan.utt/extensions/deckhand.jobs")
let part = jobs.appending(path: "clip-1.wav.part")
try wav.write(to: part)
try FileManager.default.moveItem(at: part, to: jobs.appending(path: "clip-1.wav")) // now utt sees it
let answer = jobs.appending(path: "clip-1.json")
while !FileManager.default.fileExists(atPath: answer.path) {
try await Task.sleep(for: .milliseconds(200))
}
struct Answer: Decodable { var text: String?; var error: String? }
let result = try JSONDecoder().decode(Answer.self, from: Data(contentsOf: answer))
try FileManager.default.removeItem(at: answer)
print(result.text ?? result.error ?? "")clip-1.json
{
"text": "the words that were spoken",
"raw": "um the words what were spoken",
"stages": ["cleanup", "replacements"],
"startedAt": "2026-09-09T17:04:09Z",
"finishedAt": "2026-09-09T17:04:11Z",
"startedAtMs": 1789052649182,
"finishedAtMs": 1789052651511,
"duration": 3.4,
"timings": {"decode": 1802.5, "replacements": 0.4, "cleanup": 511.2}
}
{"error": "Could not transcribe that clip.", "startedAt": "2026-09-09T17:04:09Z",
"finishedAt": "2026-09-09T17:04:11Z", "startedAtMs": 1789052649182, "finishedAtMs": 1789052651511}wav, m4a, mp3, aiff, flac, caf. De extensie bepaalt de reader, dus die moet overeenkomen met de bytes.text is de transcriptie; raw, stages, cleanupSkipped en timings beschrijven wat er is gehoord en welke stappen het hebben veranderd, precies zoals het API-antwoord, met één extra eigen stapnaam: hints, voor het onderstaande bestand. startedAt, finishedAt en hun millisecondevarianten startedAtMs/finishedAtMs zijn er altijd, bij succes én bij fouten; de rest niet.clip-1.hints.json, een platte JSON-array van strings, naast de opname. utt corrigeert de transcriptie dan aan de hand van die termen.Stel "wantsTranscripts": true in en elke transcriptie die utt produceert wordt na voltooiing naar <id>.transcript.json geschreven.
deckhand.transcript.json
{
"sequence": 12,
"text": "the words that were spoken",
"raw": "um the words what were spoken",
"stages": ["cleanup", "replacements"],
"finishedAt": "2026-09-09T16:58:03Z",
"duration": 3.4,
"timings": {"decode": 1802.5, "replacements": 0.4, "cleanup": 511.2},
"app": "Ghostty"
}sequence gaat per transcriptie omhoog. Poll het precies zoals je revision pollt.app is waar de tekst werd geplakt en ontbreekt als niets de tekst heeft ontvangen.raw, stages, cleanupSkipped en timings zijn dezelfde velden als in het antwoord van een job, behalve de stap hints, omdat dicteren geen hintsbestand heeft. Hier is geen startedAt, alleen finishedAt en duration.Elk hiervan is één extra sleutel in het manifest en één extra bestand of map die utt voor je bijhoudt.
wantsPartials: de woorden terwijl ze worden gedecodeerd en de toets nog ingedrukt is, geschreven naar <id>.partial.json. Een andere, kleinere herkenner die alleen Engels ondersteunt produceert ze vóór de tekststappen van utt en alleen wanneer de gebruiker Show words while you speak heeft ingeschakeld. De echte transcriptie is een afzonderlijke, latere gebeurtenis en kan iets heel anders zeggen. Niets hier is definitief.filtersTranscripts: elke voltooide transcriptie wordt naar <id>.filter/<name>.in.json geschreven. Wat je binnen twee seconden naar <name>.out.json terugschrijft, wordt afgeleverd. Een gemiste deadline is geen fout: het origineel gaat door. Een lege string laat de transcriptie vervallen.skipsTextStages: stappen uit de eigen tekstpipeline van de gebruiker die alleen voor jouw opnames worden overgeslagen, nooit voor diens dicteeropnames. De opties zijn replacements, cleanup en formatting. Een onbekende naam wordt weggelaten in plaats van het manifest te weigeren.actions: maximaal acht knoppen op je pagina. Een druk op een knop schrijft <id>.action.json met een sequence die je precies zo pollt als revision. Met "confirms": true vraagt utt eerst om bevestiging.daemon: een launchd-label. utt leest de status uit launchd en biedt een knop Restart. Het start, laadt of ontlaadt nooit iets namens jou. Het verwacht een label in plaats van een pad naar een plist en weigert een label dat begint met com.apple.. Je mag rapporteren over je eigen daemon, maar niet in het systeem zelf grijpen.showsInMenuBar: een submenu in het eigen menubalkmenu van utt, opgebouwd uit je statusregels, je daemon en al je acties. Alles of niets; er is geen opt-in per actie.tint, description, website en repository bepalen hoe de extensie in utt verschijnt. Ze bepalen de menubalkkleur tijdens de jobs en het onderdeel Over op de pagina. Links naar websites en repositories moeten https:// gebruiken.
Je kunt ook <id>.status.json schrijven: een plat object met strings dat bovenaan je pagina alleen-lezen wordt weergegeven.
deckhand.status.json
{"daemon": "up 0.1.0", "sessions": "9 live", "lastRelay": "2 min ago"}lastRelay 'Last relay' wordt.revision verandert. Gebruik totdat het bestand bestaat de standaardwaarden uit je eigen manifest. Het kan ontbreken omdat nog niemand je heeft goedgekeurd.needsApi alleen als je de HTTP-API voor iets anders nodig hebt, bijvoorbeeld om vanaf een ander apparaat met utt te communiceren.settings.json van utt.Zet geen eigen geheimen in het manifest. Het is een gewoon bestand en de inhoud wordt in het utt-venster getoond.
De pagina Extensions in utt heeft een knop Copy guide. Die kopieert een LLM-briefing op basis van deze handleiding en vult de extensiemap voor de huidige installatie in.