Skip to content

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 turnsSend it as one message
WhenThe block says who spoke: role labels (Agent:, Customer:) or speaker names you can sort into your team and the customerThe block has no speaker information at all
What you getEvery turn with its direction, so the agent can compare what customers said with what your team saidDimensions 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.

Check one transcript first and see how a turn starts:

  • Role labels, such as Agent: ... and Customer: ..., or BOT: and USER:. 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 .vtt file), with cues like 00:00:09.000 --> 00:00:12.000 and 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).

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.