Byg en fuldautomatisk arbejdsgang til transskribering med n8n

De fleste teams transskriberer stadig i hånden: nogen henter optagelsen, uploader den et sted, venter, henter en SRT-fil, omdøber den og lægger den i den rigtige mappe. Det går fint — lige indtil det sker fyrre gange om ugen.


Denne vejledning erstatter den person med en arbejdsgang. Så snart en ny optagelse lander, bliver den transskriberet, undertekstet og leveret — uden at nogen holder øje. Vi bruger n8n, fordi det kører alle vegne, kan hostes på din egen infrastruktur og taler med et hvilket som helst HTTP-API — også vores.


Til sidst har du en arbejdsgang, der:


  1. starter, så snart en ny video- eller lydfil dukker op
  2. sender den til Inwista-API'et til transskribering
  3. venter, til jobbet er færdigt, uden at gætte på hvor lang tid det tager
  4. henter den færdige SRT-fil
  5. leverer den dér, hvor dit team har brug for den


Alt nedenfor kører på det åbne Inwista API v1. Ingen plugins, ingen egne kodenoder.

Før du går i gang

Du skal bruge tre ting:


  • En n8n-instans — cloud eller selvhostet, version 1.x
  • En Inwista-API-nøgle — opret en under Mit arbejdsområde → API-nøgler. Den begynder med inw_live_
  • Medier, API'et kan nå — API'et tager en offentlig https-URL, så filer i Dropbox, Google Drive, S3 eller dit CMS skal have et delbart eller signeret link


Et ord om omkostningerne, før du bygger noget, der kører uden opsyn: transskribering koster 4 kreditter pr. påbegyndt medieminut, trukket når jobbet bliver taget imod. Job, der fejler, bliver refunderet automatisk. Test arbejdsgangen på et par korte filer, før du slipper den løs på hele arkivet.

Trin 1: vælg din udløser

Forløbet starter med den hændelse, der betyder »her er noget nyt, der skal transskriberes«. De mest almindelige valg:


  • Google Drive Trigger / Dropbox Trigger — en overvåget mappe, hvor dit team lægger optagelserne
  • Webhook — dit eget CMS eller LMS kalder n8n, når en upload er færdig
  • Schedule Trigger — tjek et RSS-feed, en podcastvært eller en database med ubehandlede rækker
  • Manual Trigger — til at bygge og teste, og det er dér, vi starter

Tilføj en Manual Trigger indtil videre. Skift den ud med den rigtige, når resten virker.

Uanset hvad du vælger: udløserens opgave er at levere én ting — en offentligt tilgængelig URL til mediefilen. Gem den i et felt, der hedder mediaUrl, så resten af vejledningen passer til din arbejdsgang.

Trin 2: send transskriberingen af sted

Tilføj en HTTP Request-node, der hedder Submit transcription.

  • Metode — POST
  • URL — https://api.inwista.ai/v1/transcriptions
  • Godkendelse — Generic Credential Type → Header Auth
  • Headernavn — Authorization
  • Headerværdi — Bearer inw_live_din_noegle_her
  • Send body — til, JSON


Body:

{
  "source_url": "{{ $json.mediaUrl }}",
  "language": "da",
  "diarization": true,
  "metadata": { "source": "n8n", "folder": "{{ $json.folderName }}" }
}


Tre felter, du bør forstå:

  • language er det sprog, der tales i filen — ikke det sprog, du vil have ud. At transskribere på kildesproget er det, der giver præcise tidskoder og ren tekst; oversættelsen kommer bagefter, som et selvstændigt trin på det færdige transskript. Håndterer dit forløb flere sprog, kan du udlede dette felt af udløserens mappe eller metadata.


  • diarization slår markering af talere til. Lad den være slået fra ved indhold med kun én stemme — den koster behandlingstid, du ikke har brug for.


  • metadata er dit. Op til 1 KB med hvad som helst, sendt uændret retur ved hvert opslag. Brug det til de identifikatorer, dine egne systemer går op i — et kursus-id, et sagsnummer, en episode-slug — så senere trin slipper for at rekonstruere sammenhængen.


Tilføj lige én header mere, mens du er i gang:


  • Idempotency-Key{{ $execution.id }}


Hvis n8n kører noden igen efter en netværksudfald, genkender API'et nøglen og giver dig det oprindelige job i stedet for at starte — og fakturere — et nyt.

Svaret kommer med det samme, med status: "processing" og et id. Transskriberingen er ikke færdig; den er taget imod.

Trin 3: vent på resultatet på den rigtige måde

Det er her, de fleste forløb går galt. Et fast »vent fem minutter« er for kort til en forelæsning og spild på et talememo. Spørg hellere om status.

Tilføj en Wait-node (Wait 15s) sat til 15 sekunder.

Tilføj en HTTP Request-node (Check status):

  • Metode — GET
  • URL — https://api.inwista.ai/v1/transcriptions/{{ $('Submit transcription').item.json.id }}
  • Godkendelse — samme header som før

Tilføj en IF-node (Is it done?) med denne betingelse:

{{ $json.status }}  equals  completed


Forbind false-grenen tilbage til Wait 15s. Det er din løkke: tjek, vent, tjek igen, indtil jobbet melder completed. True-grenen fører videre til næste trin.

Svaret indeholder også et progress-tal, der viser, hvor forløbet reelt står, så vil du have en fremdriftsindikator i Slack eller dit eget dashboard, ligger den der allerede.


To ting, du bør tilføje, før det her kommer i nærheden af produktion:

  • Håndtér fejl. Tilføj endnu en IF, der tjekker {{ $json.status }} equals failed og sender videre til den alarmering, du bruger. Job, der fejler, bliver refunderet automatisk, men du vil stadig gerne vide det.
  • Sæt loft over løkken. n8n's løkkebeskyttelse hjælper, men et eksplicit loft — en tæller, der giver op efter for eksempel 80 runder — gør et fastlåst job til en advarsel i stedet for en kørsel, der løber hele natten.

Trin 4: hent underteksterne

Tilføj en HTTP Request-node, der hedder Get SRT:

  • Metode — GET
  • URL — https://api.inwista.ai/v1/transcriptions/{{ $('Submit transcription').item.json.id }}/captions?format=srt
  • Response format — File (eller Text, hvis du vil have indholdet med videre i forløbet)

Endepunktet returnerer selve undertekstfilen, ikke en JSON-indpakning — så nodens output er klar til at blive skrevet til disk, vedhæftet en mail eller uploadet.

Skift format ud efter, hvad modtageren har brug for:

Format — bruges til

  • srt — videoafspillere, redigeringsprogrammer, YouTube, de fleste CMS'er
  • vtt — HTML5-<track>, webafspillere
  • txt — søgeindekser, LLM-forløb, dokumentation
  • json — tidskoder på ordniveau, markering af talere, egen visning

Hvert format er blot en visning af det samme færdige job. At hente fire af dem koster ikke noget ekstra.

Trin 5: lever resultatet

Den sidste node er det, »færdig« betyder for dit team:

  • Google Drive / Dropbox / S3 — læg filen ved siden af kildevideoen
  • Slack — post transskriptet i den kanal, der bad om det
  • HTTP Request — skub det ind i dit CMS, dit LMS eller dit undertekstfelt
  • Postgres / Airtable / Notion — gem txt-versionen som søgbar tekst

Er din modtager en af dem, Inwista allerede integrerer direkte med — Google Drive, OneDrive, SharePoint, Dropbox, Box, YouTube, Vimeo, Wistia og flere — kan du droppe denne node helt og i stedet sætte cloud-levering op under Mit arbejdsområde → Integrationer. Så kommer filerne automatisk ved hvert færdigt job, med eller uden arbejdsgang.

Næste niveau: oversæt før du leverer

Sæt to noder ind før leveringen, så giver den samme arbejdsgang dig undertekster på lige så mange sprog, du har brug for.

POST /v1/transcriptions/{id}/translate med:

{ "target_language": "en" }


Den svarer 202 Accepted — oversættelsen kører på det færdige transskript, så tidskoderne passer allerede, og kun teksten ændrer sig. Spørg GET /v1/transcriptions/{id}/translations/en på samme måde, som du spurgte om jobbet, og hent så:

GET /v1/transcriptions/{id}/captions?format=srt&language=en


Kør det gennem en liste af målsprog, og én optagelse bliver til et komplet flersproget undertekstsæt i én kørsel. Bemærk, at sprogforespørgslen er streng: beder du om et sprog uden færdig oversættelse, får du en tydelig fejl i stedet for kildesproget i stilhed. Det er præcis, hvad du vil have i et forløb, der kører uden opsyn.

Næste niveau: undertekster i sendekvalitet

Rå transskribering er ordret. Undertekster er håndværk: linjelængder, læsehastighed, dér hvor en sætning brydes over to blokke.

POST /v1/transcriptions/{id}/enhance kører det færdige transskript gennem netop den behandling — fortætter teksten, balancerer linjeskift på ny, formaterer dialog og håndhæver mindste og største blokvarighed:

{
  "settings": {
    "maxLinesPerBlock": "2",
    "maxCharactersPerLine": 42,
    "textCondensation": "smart",
    "speakerDialogueFormat": "hyphens",
    "gapBetweenBlocks": "broadcasting"
  }
}


Den svarer 202; spørg GET /v1/transcriptions/{id}/enhancements/{enhancementId}, indtil den er færdig. Bagefter serverer undertekst-endepunkterne automatisk den forbedrede version — du behøver ikke ændre noget i din leveringsnode.

Følsomme optagelser

Behandler dit forløb materiale, du helst ikke vil have, at vi beholder — patientsamtaler, juridiske optagelser, interne personalemøder — så tilføj ét felt til indsendelsen fra trin 2:

{
  "source_url": "{{ $json.mediaUrl }}",
  "language": "da",
  "retention": "none"
}


Med retention: "none" bliver kildemediet slettet, så snart transskriberingen er færdig. Transskriptet, underteksterne, oversættelserne og senere forbedringer fungerer helt som før — det er kun lyden og videoen, der er væk. Der findes også store_media: false, som beholder filen til behandlingen, men ikke laver afspilningskopier.

Og når opbevaringsperioden for et job udløber hos dig, sletter DELETE /v1/transcriptions/{id} alt, der hører til — medier, transskript, revisioner, oversættelser — med ét kald. En planlagt n8n-arbejdsgang, der sletter job ældre end din politik, er cirka fire noder, og den gør din opbevaringspolitik til noget, du kan vise frem i stedet for at beskrive.

Webhooks: modstykket

At spørge om status er den rigtige styring inde i én n8n-kørsel: den står på egne ben, kræver ingen offentlig URL og holder hele forløbet ét sted, du kan fejlsøge.

Webhooks løser et andet problem: at få færdigt arbejde frem til én central modtager, uanset hvor jobbet kom fra. Optagelser, dine kolleger uploader i dashboardet, job sendt ind fra et andet system, eksporter der bliver færdige timer senere — det hele kan lande på ét endepunkt i stedet for, at hver arbejdsgang passer sit eget. Sæt det op under Mit arbejdsområde → Integrationer.

Peg den mod en Webhook-node i n8n, og du modtager en signeret transcript.completed-hændelse:

{
  "event": "transcript.completed",
  "id": "evt_...",
  "timestamp": 1786902819,
  "workspaceId": "...",
  "project": {
    "id": "...",
    "name": "bestyrelsesmoede-august.mp4",
    "language": "da",
    "durationSeconds": 3184
  },
  "files": [
    { "format": "srt", "name": "bestyrelsesmoede-august.srt", "url": "https://...", "expiresAt": 1786989219 }
  ]
}


Hver forespørgsel er signeret med HMAC-SHA256 over den rå body, i headeren X-Inwista-Signature som sha256=<hex>. Tjek den, før du stoler på indholdet — i n8n er en Crypto-node og en IF-sammenligning nok. Fil-URL'erne er signerede og gyldige i 24 timer, så hent det, du har brug for, i stedet for at gemme linkene.

Leveringen forsøges tre gange, og et endepunkt, der bliver ved med at fejle, bliver slået fra automatisk med årsagen synlig i dine integrationsindstillinger — en ødelagt modtager dukker altså op som en status, du kan se, i stedet for hændelser, der stille forsvinder.

Ting, der før eller siden bider dig

Forespørgselsgrænser. 300 læse- og 60 skriveforespørgsler pr. minut pr. nøgle. Rigeligt til normal brug, hurtigt brugt op, hvis du slipper et helt arkiv løs parallelt uden kø. Skal du hente hundredvis af filer ind, så kør dem i portioner.

Kilde-URL'erne skal kunne nås. API'et undersøger mediefilen, før jobbet bliver taget imod — det er sådan, varighed og pris er kendt på forhånd. Et Drive-link, der kræver login, giver unreadable_source, og der bliver ikke faktureret noget. Brug direkte eller signerede URL'er.

Fejl er struktureret. Hver fejl giver den samme indpakning:

{ "error": { "code": "insufficient_credits", "message": "..." } }


Forgren på code, aldrig på beskedteksten. Koder kommer kun til inden for v1 og bliver aldrig omdøbt; beskeder kan godt blive omformuleret.

Idempotensnøgler gælder pr. forespørgsel, ikke pr. fil. De beskytter dig mod dobbelt fakturering, når den samme nodekørsel forsøges igen. At luge den samme optagelse ud, når to forskellige kørsler sender den ind, er din arbejdsgangs opgave — en opslagstabel på fil-id er det sædvanlige svar.

Den færdige arbejdsgang

Her er skelettet til at importere og tilpasse. Erstat credential-referencen med din egen Header Auth-credential i stedet for at klistre en nøgle direkte ind i noden.

{
  "name": "Inwista — automated transcription",
  "nodes": [
    {
      "parameters": {},
      "id": "trigger",
      "name": "When clicking Test workflow",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [0, 0]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.inwista.ai/v1/transcriptions",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\"source_url\": \"{{ $json.mediaUrl }}\", \"language\": \"da\", \"diarization\": true}",
        "options": {}
      },
      "id": "submit",
      "name": "Submit transcription",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [220, 0]
    },
    {
      "parameters": { "amount": 15 },
      "id": "wait",
      "name": "Wait 15s",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [440, 0]
    },
    {
      "parameters": {
        "url": "=https://api.inwista.ai/v1/transcriptions/{{ $('Submit transcription').item.json.id }}",
        "options": {}
      },
      "id": "status",
      "name": "Check status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [660, 0]
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "version": 2 },
          "conditions": [
            {
              "leftValue": "={{ $json.status }}",
              "rightValue": "completed",
              "operator": { "type": "string", "operation": "equals" }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "isdone",
      "name": "Is it done?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [880, 0]
    },
    {
      "parameters": {
        "url": "=https://api.inwista.ai/v1/transcriptions/{{ $('Submit transcription').item.json.id }}/captions?format=srt",
        "options": { "response": { "response": { "responseFormat": "text" } } }
      },
      "id": "srt",
      "name": "Get SRT",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1100, -100]
    }
  ],
  "connections": {
    "When clicking Test workflow": { "main": [[{ "node": "Submit transcription", "type": "main", "index": 0 }]] },
    "Submit transcription": { "main": [[{ "node": "Wait 15s", "type": "main", "index": 0 }]] },
    "Wait 15s": { "main": [[{ "node": "Check status", "type": "main", "index": 0 }]] },
    "Check status": { "main": [[{ "node": "Is it done?", "type": "main", "index": 0 }]] },
    "Is it done?": {
      "main": [
        [{ "node": "Get SRT", "type": "main", "index": 0 }],
        [{ "node": "Wait 15s", "type": "main", "index": 0 }]
      ]
    }
  }
}


Seks noder. Alt det andet — oversættelse, forbedring, levering, sletning — hænger på det samme skelet.

Hvad det her reelt ændrer

Det interessante er ikke, at transskribering bliver automatiseret. Det er, hvad der holder op med at være en beslutning.


Når undertekster koster en eftermiddag af en persons tid, bliver de rationeret: de vigtige videoer får dem, resten gør ikke. Når de ikke koster noget pr. fil og er der, før nogen når at spørge, holder de op med at være et projekt og bliver en egenskab ved dit indhold — hver optagelse søgbar, hver video tilgængelig, hvert kursus i overensstemmelse med kravene, uden at der skal et møde til.


Det er mere værd end de timer, det sparer.


Klar til at bygge det? Opret en API-nøgle — gratisniveauet rækker til at køre hele forløbet igennem. Den fulde endepunktsreference finder du i API-dokumentationen.