Wie du das vollständige Transkript einer Aufnahme abrufst
Das Transkript ist die Grundlage, aus der Sally die Zusammenfassung erstellt: wer wann was gesagt hat. Diese Anleitung zeigt, wie du prüfst, ob ein Transkript existiert, was die Antwort enthält, wie du sie klein hältst, wie du es als fertige Datei oder als Untertitel herunterlädst, und endet mit einem Skript, das aus einer Aufnahme eine lesbare Textdatei macht.
Jedes Beispiel funktioniert so, wie es dasteht, sobald SALLY_TOKEN, DIRECTORY_ID und RECORDING_ID gesetzt sind, siehe Schnellstart. Wähl deine Sprache einmal, alle Beispiele im API-Bereich folgen ihr.
Lies zuerst den Schnellstart und Termine, Aufnahmen und Zusammenfassungen.
Schnellnavigation
- Vorher den Status prüfen
- Transkript abrufen
- Die Antwort klein halten
- Als Datei herunterladen
- Vollständiges Skript: Transkript als Text exportieren
- Endpunkte im Überblick
1. Vorher den Status prüfen
Ein beendeter Transkriptionslauf ist nicht dasselbe wie ein vorhandenes Transkript. Sieh dir zuerst die Aufnahme an. Mit der Feldauswahl beschränkst du die Antwort auf die drei Statusfelder:
- cURL
- JavaScript
- Python
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID?fields=isTranscriptionCompleted,isTranscriptionSucceeded,transcriptionCompletionReason" \
-H "Authorization: Bearer $SALLY_TOKEN"
const url = new URL(
`https://api.sally.io/v1.0/directories/${directoryId}/recordings/${recordingId}`,
);
url.searchParams.set(
'fields',
'isTranscriptionCompleted,isTranscriptionSucceeded,transcriptionCompletionReason',
);
const response = await fetch(url, {
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`},
});
const status = await response.json();
import os
import requests
response = requests.get(
f"https://api.sally.io/v1.0/directories/{directory_id}/recordings/{recording_id}",
params={"fields": "isTranscriptionCompleted,isTranscriptionSucceeded,transcriptionCompletionReason"},
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
status = response.json()
{
"isTranscriptionCompleted": true,
"isTranscriptionSucceeded": true,
"transcriptionCompletionReason": "succeeded"
}
| Feld | Typ | Bedeutung |
|---|---|---|
isTranscriptionCompleted | boolean | Der Lauf ist vorbei, wie auch immer er ausging. |
isTranscriptionSucceeded | boolean | Es gibt ein brauchbares Transkript. Dieses Feld solltest du prüfen. |
transcriptionCompletionReason | string | Warum der Lauf endete, siehe die nächste Tabelle. |
transcriptionCompletionReason | Bedeutung |
|---|---|
succeeded | Es gibt ein brauchbares Transkript. |
noSpokenAudio | In der Aufnahme hat niemand gesprochen. |
fileEmpty | Die Datei war leer. |
languageNotDetected | Die Sprache konnte nicht erkannt werden. |
quotaExceeded | Das Transkriptionskontingent war aufgebraucht. |
error | Der Lauf ist fehlgeschlagen. Der eigentliche Fehlertext wird bewusst nicht herausgegeben. |
unknown | Der Lauf ist noch nicht beendet. |
Ein Meeting, in dem niemand gesprochen hat, ergibt einen beendeten Lauf ohne Transkript. Das ist kein Fehler und braucht keinen zweiten Versuch, noSpokenAudio sagt es deutlich.
2. Transkript abrufen
Sobald isTranscriptionSucceeded gleich true ist, rufst du das Transkript ab:
- cURL
- JavaScript
- Python
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID/transcription" \
-H "Authorization: Bearer $SALLY_TOKEN"
const response = await fetch(
`https://api.sally.io/v1.0/directories/${directoryId}/recordings/${recordingId}/transcription`,
{headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`}},
);
const {speakers, segments} = await response.json();
import os
import requests
response = requests.get(
f"https://api.sally.io/v1.0/directories/{directory_id}/recordings/{recording_id}/transcription",
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
transcript = response.json()
{
"recordingId": "53e2e041-4b34-41c7-a285-634d80b3675d",
"speakers": [
{
"speakerId": "b16c2c6f-d9f5-4709-a27e-41a319410893",
"speakerNumber": 1,
"name": "Anna Berger",
"emailAddress": "anna.berger@example.com"
},
{
"speakerId": "4f0e1c52-7a3b-4d8e-9c61-2b5d8e7f3a10",
"speakerNumber": 2,
"name": null,
"emailAddress": null
}
],
"segments": [
{
"speakerId": "b16c2c6f-d9f5-4709-a27e-41a319410893",
"startTime": 12.4,
"endTime": 17.9,
"text": "Wir haben uns auf den Rollout-Plan für Oktober geeinigt."
},
{
"speakerId": "4f0e1c52-7a3b-4d8e-9c61-2b5d8e7f3a10",
"startTime": 18.2,
"endTime": 21.0,
"text": "Dann passe ich den Zeitplan heute noch an."
}
]
}
Die Antwort enthält zwei Listen, die zusammengehören:
speakersenthält je Stimme einen Eintrag mitspeakerId,speakerNumberund, wo Sally es auflösen konnte,nameundemailAddress.segmentsenthält die zeitcodierten Sprachabschnitte mitstartTimeundendTimein Sekunden, demtextund einerspeakerIdaus der Listespeakers. Für den Sprecher eines Segments brauchst du also keinen zweiten Aufruf.
| Feld | Typ | Beschreibung |
|---|---|---|
recordingId | string | Id der Aufnahme. |
speakers[].speakerId | string | Id des Sprechers, auf die jedes Segment verweist. |
speakers[].speakerNumber | number | Nummer des Sprechers. |
speakers[].name | string | null | Name des Sprechers. |
speakers[].emailAddress | string | null | E-Mail des Sprechers. |
segments[].speakerId | string | Sprecher des Segments, verweist auf die Liste speakers. |
segments[].startTime | number | Beginn in Sekunden. |
segments[].endTime | number | Ende in Sekunden. |
segments[].text | string | null | Transkribierter Text des Segments. |
3. Die Antwort klein halten
Transkripte sind die größten Antworten dieser API, und fast alles davon steckt in segments. Wenn du nur wissen willst, wer teilgenommen hat, frag ?fields=speakers ab. Die Segmente werden dann gar nicht erst gesendet. Die Regeln erklärt Feldauswahl.
- cURL
- JavaScript
- Python
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID/transcription?fields=speakers" \
-H "Authorization: Bearer $SALLY_TOKEN"
const url = new URL(
`https://api.sally.io/v1.0/directories/${directoryId}/recordings/${recordingId}/transcription`,
);
url.searchParams.set('fields', 'speakers');
const response = await fetch(url, {
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`},
});
const {speakers} = await response.json();
import os
import requests
response = requests.get(
f"https://api.sally.io/v1.0/directories/{directory_id}/recordings/{recording_id}/transcription",
params={"fields": "speakers"},
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
speakers = response.json()["speakers"]
{
"speakers": [
{
"speakerId": "b16c2c6f-d9f5-4709-a27e-41a319410893",
"speakerNumber": 1,
"name": "Anna Berger",
"emailAddress": "anna.berger@example.com"
}
]
}
Auswählbar sind an diesem Endpunkt recordingId, speakers und segments. Eine verschachtelte Liste kommt ganz oder gar nicht, speakers.name wird nicht unterstützt.
4. Als Datei herunterladen
Wenn du das Transkript als Dokument statt als Daten brauchst, liefert GET …/recordings/{recordingId}/transcription/document genau die Datei, die ein Nutzer in der Sally-Weboberfläche herunterlädt: mit Branding, Logos, Kopf- und Fußzeile, Sprechern, Zeitstempeln und demselben Dateinamen. Die Datei wird bei der Anfrage erzeugt und kommt direkt in der Antwort zurück, es gibt keinen Auftrag, den du abfragen musst. Die Formate vtt und srt sind Untertiteldateien ohne Kopfbereich, direkt bereit für einen Videoplayer.
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
format | string | ja | pdf, docx, xlsx, txt, md, vtt oder srt. |
languageCode | string | nein | de-DE, en-US, es-ES, fr-FR, it-IT, nl-NL oder pl-PL. Ohne Angabe gilt die Sprache des Token-Inhabers, wie in der Weboberfläche. |
- cURL
- JavaScript
- Python
# -OJ speichert die Datei unter dem Namen aus Content-Disposition
curl -fOJ "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID/transcription/document?format=docx&languageCode=de-DE" \
-H "Authorization: Bearer $SALLY_TOKEN"
import {writeFile} from 'node:fs/promises';
const url = new URL(
`https://api.sally.io/v1.0/directories/${directoryId}/recordings/${recordingId}/transcription/document`,
);
url.searchParams.set('format', 'docx');
url.searchParams.set('languageCode', 'de-DE');
const response = await fetch(url, {
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`},
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
// Derselbe Dateiname wie in der Weboberfläche, UTF-8-Fassung aus Content-Disposition
const disposition = response.headers.get('content-disposition') ?? '';
const match = /filename\*=UTF-8''([^;]+)/i.exec(disposition);
const fileName = match ? decodeURIComponent(match[1]) : 'transkript.docx';
await writeFile(fileName, Buffer.from(await response.arrayBuffer()));
import os
import re
from urllib.parse import unquote
import requests
response = requests.get(
f"https://api.sally.io/v1.0/directories/{directory_id}/recordings/{recording_id}/transcription/document",
params={"format": "docx", "languageCode": "de-DE"},
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
response.raise_for_status()
# Derselbe Dateiname wie in der Weboberfläche, UTF-8-Fassung aus Content-Disposition
disposition = response.headers.get("Content-Disposition", "")
match = re.search(r"filename\*=UTF-8''([^;]+)", disposition, re.IGNORECASE)
file_name = unquote(match.group(1)) if match else "transkript.docx"
with open(file_name, "wb") as f:
f.write(response.content)
languageCode ändert nur die Beschriftungen und das Datumsformat im Dokument, nicht den transkribierten Text. Die Schrift richtet sich immer nach der Sprache der Aufnahme, chinesische Schriftzeichen bleiben also auch mit en-US-Beschriftungen lesbar. ?fields= wird bei diesem Endpunkt nicht unterstützt.
Die Antwort ist 200 mit der Datei selbst, kein JSON. Der Header Content-Disposition enthält den Dateinamen, den die Weboberfläche für denselben Download verwendet, in ASCII in filename und in UTF-8 in filename*. Der Content-Type richtet sich nach dem Format:
format | Content-Type |
|---|---|
pdf | application/pdf |
docx | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
xlsx | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
txt | text/plain; charset=utf-8 |
md | text/markdown; charset=utf-8 |
vtt | text/vtt; charset=utf-8 |
srt | application/x-subrip; charset=utf-8 |
Neben den üblichen Fehlern antwortet dieser Endpunkt mit:
| Status | code | Bedeutung |
|---|---|---|
400 | UNKNOWN_DOCUMENT_FORMAT oder UNKNOWN_LANGUAGE_CODE | format fehlt oder ist unbekannt, oder languageCode ist unbekannt. Der Body nennt die erlaubten Werte. |
403 | TRANSCRIPT_DOWNLOAD_PREVENTED | Das Unternehmenskonto hat diesen Download für seine Mitglieder gesperrt. |
404 | Die Aufnahme gibt es nicht, sie gehört zu einem anderen Unternehmenskonto, oder du hast keinen Zugriff darauf. | |
409 | TRANSCRIPT_NOT_AVAILABLE | Die Aufnahme hat noch kein Transkript. Prüf vorher den Status. |
Unter Fehler siehst du, wie du diese Codes an einer Stelle behandelst.
5. Vollständiges Skript: Transkript als Text exportieren
Dieses Skript wartet, bis der Transkriptionslauf einer Aufnahme beendet ist, hört sauber auf, wenn es kein Transkript gibt, und schreibt sonst eine lesbare Textdatei mit einer Zeile je Segment: Zeitstempel, Sprecher, Text. Sprecher ohne aufgelösten Namen erscheinen als Speaker 1, Speaker 2 und so weiter. Das Abfrageintervall von 30 Sekunden hat das Skript gewählt, es ist kein Wert der API.
- cURL
- JavaScript
- Python
#!/usr/bin/env bash
# Braucht curl und jq. curl --retry wiederholt eine Anfrage nach einem 429 mit wachsenden Pausen.
set -euo pipefail
BASE="https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings/$RECORDING_ID"
CURL=(curl -sf --retry 5 -H "Authorization: Bearer $SALLY_TOKEN")
# 1. Warten, bis der Lauf beendet ist
while : ; do
status=$("${CURL[@]}" "$BASE?fields=isTranscriptionCompleted,isTranscriptionSucceeded,transcriptionCompletionReason")
[ "$(echo "$status" | jq '.isTranscriptionCompleted')" = "true" ] && break
sleep 30
done
# 2. Aufhören, wenn es kein Transkript gibt
if [ "$(echo "$status" | jq '.isTranscriptionSucceeded')" != "true" ]; then
echo "Kein Transkript: $(echo "$status" | jq -r '.transcriptionCompletionReason')"
exit 0
fi
# 3. Abrufen und eine Zeile je Segment schreiben
"${CURL[@]}" "$BASE/transcription" | jq -r '
(.speakers | map({key: .speakerId, value: (.name // "Speaker \(.speakerNumber)")}) | from_entries) as $names
| .segments[]
| (.startTime | floor) as $s
| "[\($s / 60 | floor):\($s % 60 | tostring | ("0" + .)[-2:])] \($names[.speakerId]): \(.text // "")"
' > "transcript-$RECORDING_ID.txt"
import {writeFile} from 'node:fs/promises';
const BASE = `https://api.sally.io/v1.0/directories/${process.env.DIRECTORY_ID}/recordings/${process.env.RECORDING_ID}`;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function api(url) {
for (let wait = 1000; ; wait = Math.min(wait * 2, 60_000)) {
const response = await fetch(url, {
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`},
});
if (response.status === 429) {
await sleep(wait);
continue;
}
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
return response.json();
}
}
const clock = (seconds) => {
const s = Math.floor(seconds);
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
};
// 1. Warten, bis der Lauf beendet ist
let status;
for (;;) {
status = await api(
`${BASE}?fields=isTranscriptionCompleted,isTranscriptionSucceeded,transcriptionCompletionReason`,
);
if (status.isTranscriptionCompleted) break;
await sleep(30_000);
}
// 2. Aufhören, wenn es kein Transkript gibt
if (!status.isTranscriptionSucceeded) {
console.log(`Kein Transkript: ${status.transcriptionCompletionReason}`);
process.exit(0);
}
// 3. Abrufen und eine Zeile je Segment schreiben
const {speakers, segments} = await api(`${BASE}/transcription`);
const names = new Map(
speakers.map((s) => [s.speakerId, s.name ?? `Speaker ${s.speakerNumber}`]),
);
const lines = segments.map(
(seg) => `[${clock(seg.startTime)}] ${names.get(seg.speakerId)}: ${seg.text ?? ''}`,
);
await writeFile(`transcript-${process.env.RECORDING_ID}.txt`, lines.join('\n') + '\n');
import os
import sys
import time
import requests
BASE = (
f"https://api.sally.io/v1.0/directories/{os.environ['DIRECTORY_ID']}"
f"/recordings/{os.environ['RECORDING_ID']}"
)
def api(url, params=None):
wait = 1
while True:
response = requests.get(
url,
params=params,
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
if response.status_code == 429:
time.sleep(wait)
wait = min(wait * 2, 60)
continue
response.raise_for_status()
return response.json()
def clock(seconds):
s = int(seconds)
return f"{s // 60}:{s % 60:02d}"
# 1. Warten, bis der Lauf beendet ist
while True:
status = api(BASE, {
"fields": "isTranscriptionCompleted,isTranscriptionSucceeded,transcriptionCompletionReason",
})
if status["isTranscriptionCompleted"]:
break
time.sleep(30)
# 2. Aufhören, wenn es kein Transkript gibt
if not status["isTranscriptionSucceeded"]:
print(f"Kein Transkript: {status['transcriptionCompletionReason']}")
sys.exit(0)
# 3. Abrufen und eine Zeile je Segment schreiben
transcript = api(f"{BASE}/transcription")
names = {
s["speakerId"]: s["name"] or f"Speaker {s['speakerNumber']}"
for s in transcript["speakers"]
}
with open(f"transcript-{os.environ['RECORDING_ID']}.txt", "w", encoding="utf-8") as f:
for seg in transcript["segments"]:
f.write(f"[{clock(seg['startTime'])}] {names[seg['speakerId']]}: {seg['text'] or ''}\n")
Das Ergebnis sieht so aus:
[0:12] Anna Berger: Wir haben uns auf den Rollout-Plan für Oktober geeinigt.
[0:18] Speaker 2: Dann passe ich den Zeitplan heute noch an.
6. Endpunkte im Überblick
Jeder Eintrag führt in die Referenz, wo du die Anfrage mit deinem eigenen Token abschicken kannst.
- GET
…/recordings/{recordingId}Gibt eine einzelne Aufnahme zurückAusprobieren - GET
…/recordings/{recordingId}/transcriptionGibt das Transkript einer Aufnahme zurück (zeitcodierte Segmente plus Sprecher)Ausprobieren - GET
…/recordings/{recordingId}/transcription/documentGibt das Transkript einer Aufnahme als fertige Dokumentdatei zurückAusprobieren