Skip to main content

Live Transcripts

Stream real-time transcription of emergency calls via WebSocket.

Prerequisites

How to Connect

  1. Obtain an access token
  2. Connect to the WebSocket using the anycable package
  3. Subscribe to the PartnerEventsChannel with your incident ID
  4. 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

PropertyDescription
wordThe transcribed word
speaker"Caller" or "CallTaker"
confidenceTranscription confidence (0-1)
started_atISO timestamp when word started
ended_atISO timestamp when word ended
durationWord duration in seconds
start_offsetSeconds from call start
language_codeDetected 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>