02 Get your data in /Integrations
Transcripts in one block
When a vendor's API gives a whole call or chat as one block of text, split it into turns, or send it as one message.
Some vendors return a conversation as one block of text rather than one record per message: a call transcript as a single string, a VTT caption file, a chat log in one field. The universal API wants one message per turn, each with its direction, so you have two ways to send it.
| Split it into turns | Send it as one message | |
|---|---|---|
| When | The block says who spoke: role labels (Agent:, Customer:) or speaker names you can sort into your team and the customer | The block has no speaker information at all |
| What you get | Every turn with its direction, so the agent can compare what customers said with what your team said | Dimensions are still extracted from the whole conversation, but it counts as one message from one side |
Split whenever you can. Send one message when there is nothing to split on.
Option 1: split it into turns
Section titled “Option 1: split it into turns”Check one transcript first and see how a turn starts:
- Role labels, such as
Agent: ...andCustomer: ..., orBOT:andUSER:. The label is the direction. - Speaker names, such as
Sam Rivera: ..., sometimes after a time offset ([00:00:09] Sam Rivera: ...). A name doesn’t say which side the speaker is on: compare it with your team’s names, or with the agent’s name the vendor returns with the call. Don’t assume the first speaker is the customer; your agent often opens the call. - WebVTT (a
.vttfile), with cues like00:00:09.000 --> 00:00:12.000and the speaker as<v Sam Rivera>or a name prefix on the cue’s text.
This function handles all three. Copy it into your guide’s script, next to the other helpers.
// transcript.mjs. Split one block of transcript text into turns.// isCompany(speaker) returns true for your agent, bot or staff, false for the customer.// isSpeaker(label), optional: false for a label that isn't a speaker ("Note: ..."), so the line continues the turn.// Returns [{ speaker, incoming, text, offsetMs }] in order. offsetMs is null when the block has no times.export function splitTranscript(block, isCompany, isSpeaker = () => true) { const turns = []; let cueStart = null; const toMs = (h, m, s, frac = '0') => ((Number(h || 0) * 60 + Number(m)) * 60 + Number(s)) * 1000 + Number(frac.padEnd(3, '0').slice(0, 3)); for (const raw of String(block).split(/\r?\n/)) { const line = raw.trim(); if (!line || line === 'WEBVTT' || /^\d+$/.test(line) || /^NOTE\b/.test(line)) continue; const cue = line.match(/^(?:(\d+):)?(\d{1,2}):(\d{2})[.,](\d{1,3})\s*-->/); // VTT or SRT cue timing if (cue) { cueStart = toMs(cue[1], cue[2], cue[3], cue[4]); continue; } let rest = line, offsetMs = cueStart; const stamp = rest.match(/^\[?(?:(\d+):)?(\d{1,2}):(\d{2})\]?\s+/); // [00:00:09] or 00:09 if (stamp) { offsetMs = toMs(stamp[1], stamp[2], stamp[3]); rest = rest.slice(stamp[0].length); } const voice = rest.match(/^<v(?:\.[^ >]*)?\s+([^>]+)>\s*/); // VTT voice tag let label = !voice && rest.match(/^([^:]{1,60}):\s+/); // "Agent: " or "Sam Rivera: " if (label && !isSpeaker(label[1].trim())) label = null; const speaker = voice ? voice[1].trim() : label ? label[1].trim() : null; const text = rest.slice(voice ? voice[0].length : label ? label[0].length : 0).replace(/<\/?[^>]+>/g, '').trim(); if (!text) continue; const last = turns[turns.length - 1]; if (speaker === null || (last && last.speaker === speaker)) { if (last) last.text += ' ' + text; // a line with no speaker, or the same speaker again, continues the turn continue; // text before the first speaker is skipped } turns.push({ speaker, incoming: !isCompany(speaker), text, offsetMs }); cueStart = null; } return turns;}Decide isCompany from what your vendor gives you:
// Role labels:const ROLES = { agent: true, bot: true, rep: true, customer: false, user: false, caller: false };const isCompany = (s) => ROLES[s.toLowerCase()] ?? false;const isSpeaker = (s) => s.toLowerCase() in ROLES; // pass as the third argument: other labels continue the turn
// Speaker names: your team's names, or the agent's name the vendor returns with the call.const TEAM = new Set((process.env.COMPANY_SPEAKERS ?? '').split(',').map((s) => s.trim().toLowerCase()));const isCompanyByName = (s) => TEAM.has(s.toLowerCase());Then send each turn as its own message, in order, at least one second apart:
const callStart = toMillis(call.startTime);let last = 0;for (const turn of splitTranscript(call.transcript, isCompany)) { const ts = Math.max(callStart + (turn.offsetMs ?? 0), last + 1000); last = ts; await sendToDimensionLabs({ text: turn.text, userId: customerId ?? `vendor-${call.id}`, // the customer's id on every turn, never a name incoming: turn.incoming, sessionId: `vendor-${call.id}`, conversationId: `vendor-${call.id}`, dimensionlabs_timestamp: ts, platformJson: { speaker_role: turn.incoming ? 'customer' : 'company' }, }, { historical: historical || ts < Date.now() - HOUR });}Personal data. Speaker names are personal data: use them only to decide the direction, and don’t send them. A speaker the function can’t place is treated as the customer, so check a few calls, and add missing names or labels. Redact the text itself as well. See PII redaction.
For a file export with the transcript in a column, the same idea is in two recipes: Split a transcript into turns (role labels) and Turn call transcripts into turns (speaker names).
Option 2: send it as one message
Section titled “Option 2: send it as one message”When the block carries no speaker information, send the whole conversation as a single message:
await sendToDimensionLabs({ text: call.transcript.replace(/\s+/g, ' ').trim(), userId: customerId ?? `vendor-${call.id}`, incoming: true, // it holds the customer's words sessionId: `vendor-${call.id}`, conversationId: `vendor-${call.id}`, dimensionlabs_timestamp: toMillis(call.startTime), platformJson: { transcript: 'unsplit', channel: 'voice' },}, { historical: historical || toMillis(call.startTime) < Date.now() - HOUR });- Dimensions still work. Enrichment reads a whole session at once, so topics, reasons, sentiment and the rest are extracted from the full transcript.
- What you lose. With no turns, nothing separates what the customer said from what your agent said, and message counts don’t mean much. Mark these records (
transcript: 'unsplit'above) so you can filter them, and say in your custom prompts that the text is a full two-sided transcript. - Switch later. If the vendor adds speaker information, or you find a way to tell the sides apart, send the turns under the same
sessionId.
Need help with a format that doesn’t fit either option? Email support@dimensionlabs.io with one sample transcript, with personal details removed.