Live Transcripts
Stream real-time transcription of emergency calls via WebSocket.
Prerequisites
incident_eventsscope (see Authentication)- Access token from
/oauth/token - Incident ID
How to Connect
- Obtain an access token
- Connect to the WebSocket using the anycable package
- Subscribe to the
PartnerEventsChannelwith your incident ID - Process incoming transcription events
WebSocket URL: wss://api.prepared911.com/cable
Event Payload
Transcription events contain an array of words with metadata:
{
"type": "transcript",
"words": [
{
"word": "Okay.",
"speaker": "CallTaker",
"confidence": 0.988,
"started_at": "2025-05-23T00:08:17.426+00:00",
"ended_at": "2025-05-23T00:08:17.906+00:00",
"duration": 0.48,
"start_offset": 12.57,
"language_code": "en-US",
"diarized_speaker": 0,
"diarized_speaker_confidence": 1
}
]
}
Word Properties
| Property | Description |
|---|---|
word | The transcribed word |
speaker | "Caller" or "CallTaker" |
confidence | Transcription confidence (0-1) |
started_at | ISO timestamp when word started |
ended_at | ISO timestamp when word ended |
duration | Word duration in seconds |
start_offset | Seconds from call start |
language_code | Detected language |
Handling Updates
When new words arrive, they may override previous words from the same speaker. If an incoming word has an earlier started_at than existing words from the same speaker, discard the overlapping words.
const processTranscriptionEvent = (newWords) => {
if (!newWords?.length) return;
const newStartTime = new Date(newWords[0].started_at);
words = [
// Filter out overwritten words
...words.filter(word =>
word.speaker !== newWords[0].speaker ||
new Date(word.started_at) < newStartTime
),
...newWords
];
};
Complete Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Live Transcript</title>
<style>
.transcript { display: flex; flex-direction: column; gap: 0.5rem; }
.message { padding: 0.5rem; border-radius: 5px; }
.dispatch { background-color: #f0f8ff; }
.caller { background-color: #ffe4e1; }
</style>
</head>
<body>
<h1>Live Transcript</h1>
<form id="incident-form">
<label for="incident-id">Incident ID:</label>
<input type="text" id="incident-id" required>
<button type="submit">Connect</button>
</form>
<div id="transcript" class="transcript"></div>
<script type="module">
import { createCable } from "https://cdn.jsdelivr.net/npm/@anycable/web/+esm";
const API_WEBSOCKET_URL = "wss://api.prepared911.com/cable";
const accessToken = "<YOUR_ACCESS_TOKEN>";
let cable = null;
let words = [];
const transcriptDiv = document.getElementById("transcript");
const renderTranscript = () => {
const sorted = words.toSorted((a, b) =>
new Date(a.started_at) - new Date(b.started_at)
);
transcriptDiv.innerHTML = "";
// Group into speech bubbles
let currentSpeaker = null;
let currentBubble = [];
sorted.forEach(word => {
if (word.speaker !== currentSpeaker) {
if (currentBubble.length) {
const div = document.createElement("div");
div.className = `message ${currentSpeaker === "Caller" ? "caller" : "dispatch"}`;
div.textContent = currentBubble.map(w => w.word).join(" ");
transcriptDiv.appendChild(div);
}
currentSpeaker = word.speaker;
currentBubble = [word];
} else {
currentBubble.push(word);
}
});
// Render final bubble
if (currentBubble.length) {
const div = document.createElement("div");
div.className = `message ${currentSpeaker === "Caller" ? "caller" : "dispatch"}`;
div.textContent = currentBubble.map(w => w.word).join(" ");
transcriptDiv.appendChild(div);
}
};
const processTranscriptionEvent = (newWords) => {
if (!newWords?.length) return;
const newStartTime = new Date(newWords[0].started_at);
words = [
...words.filter(word =>
word.speaker !== newWords[0].speaker ||
new Date(word.started_at) < newStartTime
),
...newWords
];
renderTranscript();
};
document.getElementById("incident-form").addEventListener("submit", (e) => {
e.preventDefault();
const incidentId = document.getElementById("incident-id").value;
if (cable) cable.disconnect();
words = [];
renderTranscript();
cable = createCable(`${API_WEBSOCKET_URL}?token=${accessToken}`);
const channel = cable.subscribeTo("PartnerEventsChannel", {
incident_id: incidentId
});
channel.on("message", (event) => {
if (event.type === "transcription_event" && event.payload?.words) {
processTranscriptionEvent(event.payload.words);
}
});
channel.on("error", (error) => console.error("Channel error:", error));
cable.on("disconnect", (reason) => console.log("Disconnected:", reason));
});
</script>
</body>
</html>