Skip to content

02 Get your data in

Turn call transcripts into turns

Recipe: split call transcripts that name each speaker into one row per turn, with the right direction and real timing.

Your call recording or meeting tool exports one row per call with the transcript in a cell, and each line starts with the speaker’s name, sometimes after a time offset:

[00:00:09] Priya Shah: Mostly good. The last two deliveries were late.

Unlike a BOT:/USER: transcript, a name doesn’t say which side the speaker is on. A common shortcut, “the first person to speak is the customer”, is wrong whenever your rep opens the call, which is most calls.

Example input, calls.csv. Sam Rivera and Dev Okafor work for Lumen Coffee; the other speakers are customers. The second call has no time offsets.

calls.csv
call_id,started_at,account_id,outcome,transcript
call-9001,2026-02-10T15:00:00Z,ACCT-204,renewed,"[00:00:02] Sam Rivera: Thanks for joining. How is the wholesale order going?
[00:00:09] Priya Shah: Mostly good. The last two deliveries were late.
[00:00:21] Sam Rivera: Sorry about that. We moved your route to a morning slot.
[00:00:30] Priya Shah: Great. Then we will renew for another year.
[00:00:31] Dev Okafor: I'll send the paperwork today."
call-9002,2026-02-12T18:30:00Z,ACCT-311,churn risk,"Jo Park: We're comparing prices with another roaster.
Sam Rivera: What would it take to stay?
Jo Park: A lower price on the espresso blend."
call-turns.csv
sessionId,userId,incoming,text,dimensionlabs_timestamp,conversationId,speaker_role,account_id,outcome,channel
call-9001,call-9001-s1,false,Thanks for joining. How is the wholesale order going?,1770735602000,call-9001,company,ACCT-204,renewed,phone
call-9001,call-9001-s2,true,Mostly good. The last two deliveries were late.,1770735609000,call-9001,customer,ACCT-204,renewed,phone
call-9001,call-9001-s1,false,Sorry about that. We moved your route to a morning slot.,1770735621000,call-9001,company,ACCT-204,renewed,phone
call-9001,call-9001-s2,true,Great. Then we will renew for another year.,1770735630000,call-9001,customer,ACCT-204,renewed,phone
call-9001,call-9001-s3,false,I'll send the paperwork today.,1770735631000,call-9001,company,ACCT-204,renewed,phone
call-9002,call-9002-s1,true,We're comparing prices with another roaster.,1770921000000,call-9002,customer,ACCT-311,churn risk,phone
call-9002,call-9002-s2,false,What would it take to stay?,1770921001000,call-9002,company,ACCT-311,churn risk,phone
call-9002,call-9002-s1,true,A lower price on the espresso blend.,1770921002000,call-9002,customer,ACCT-311,churn risk,phone
  • Direction comes from a list of your people. Put everyone who speaks for your company in COMPANY. Everyone else is the customer (incoming: true). The script prints the names it treated as customers so you can catch a colleague who is missing from the list.
  • Each participant keeps their own userId. In a call between people, each participant has their own id. The script numbers speakers in order of appearance (call-9001-s1, -s2), so ids are stable across reruns and no names go into the file. A speaker_role column says company or customer.
  • Real timing when you have it. A [hh:mm:ss] offset is added to the call’s start time. Without offsets, each turn is one second after the one before. Either way, turns stay at least a second apart and in order.
  • One call is one session. Use the call id as sessionId and conversationId, as call transcripts need.
  • Call-level fields ride along. The account id and outcome become extra columns, which lets the agent join calls to business data by account.
  • Summaries are not turns. If your tool also exports an AI summary or “next steps” text, keep it out of the turns; it was not said by anyone on the call. Keep it as a column if you want it.

Node 18 or later, with npm install papaparse.

call-turns.mjs
// Turn call transcripts with named speakers into one row per turn.
// Usage: node call-turns.mjs calls.csv call-turns.csv
import fs from 'node:fs';
import Papa from 'papaparse';
const [input, output] = process.argv.slice(2);
// Everyone who speaks for your company. Anyone else is the customer (incoming).
const COMPANY = new Set(['sam rivera', 'dev okafor']);
// "[00:01:15] Name: text" or "Name: text". The name can't contain a colon.
const TURN = /^\s*(?:\[(\d{1,2}):(\d{2}):(\d{2})\]\s*)?([^:\[\]]{1,60}):\s+(.*)$/;
const { data } = Papa.parse(fs.readFileSync(input, 'utf8'), { header: true, skipEmptyLines: 'greedy' });
const rows = [];
const customerNames = new Set();
for (const call of data) {
const start = Date.parse(call.started_at);
const speakers = []; // order of first appearance, for stable ids without names
let last = start - 1000;
for (const line of call.transcript.split(/\r?\n/)) {
const m = line.match(TURN);
if (!m) {
if (rows.length && line.trim() && rows.at(-1).sessionId === call.call_id) rows.at(-1).text += ' ' + line.trim();
continue;
}
const [, h, mi, s, rawName, said] = m;
const name = rawName.trim();
if (!speakers.includes(name)) speakers.push(name);
const fromCompany = COMPANY.has(name.toLowerCase());
if (!fromCompany) customerNames.add(name);
// Offset from the start of the call when the transcript has one, otherwise one second per turn.
let ts = h !== undefined ? start + ((+h * 60 + +mi) * 60 + +s) * 1000 : last + 1000;
if (ts < last + 1000) ts = last + 1000; // keep turns at least a second apart, in order
last = ts;
rows.push({
sessionId: call.call_id,
userId: `${call.call_id}-s${speakers.indexOf(name) + 1}`, // each participant keeps their own id
incoming: fromCompany ? 'false' : 'true',
text: said.trim(),
dimensionlabs_timestamp: ts,
conversationId: call.call_id,
speaker_role: fromCompany ? 'company' : 'customer',
account_id: call.account_id,
outcome: call.outcome,
channel: 'phone',
});
}
}
fs.writeFileSync(output, Papa.unparse(rows.filter((r) => r.text), { newline: '\n' }) + '\n');
console.log(`${data.length} calls -> ${rows.length} turns`);
console.log(`Treated as customers (check this list): ${[...customerNames].join(', ')}`);
Terminal window
node call-turns.mjs calls.csv call-turns.csv
# 2 calls -> 8 turns
# Treated as customers (check this list): Priya Shah, Jo Park
  • A participants column with emails: build COMPANY from it instead of typing names, by treating every participant whose email ends in your company’s domain as company.
  • Speaker labels like Speaker 1: you need a mapping per call (often in another column of the export) from speaker number to person. Without it, direction is a guess; ask support before uploading.
  • Times like 01:15 (minutes and seconds): change the pattern to \[(\d{1,2}):(\d{2})\] and the offset to (+mm * 60 + +ss) * 1000.