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.
When you need it
Section titled “When you need it”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.
call_id,started_at,account_id,outcome,transcriptcall-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."Output
Section titled “Output”sessionId,userId,incoming,text,dimensionlabs_timestamp,conversationId,speaker_role,account_id,outcome,channelcall-9001,call-9001-s1,false,Thanks for joining. How is the wholesale order going?,1770735602000,call-9001,company,ACCT-204,renewed,phonecall-9001,call-9001-s2,true,Mostly good. The last two deliveries were late.,1770735609000,call-9001,customer,ACCT-204,renewed,phonecall-9001,call-9001-s1,false,Sorry about that. We moved your route to a morning slot.,1770735621000,call-9001,company,ACCT-204,renewed,phonecall-9001,call-9001-s2,true,Great. Then we will renew for another year.,1770735630000,call-9001,customer,ACCT-204,renewed,phonecall-9001,call-9001-s3,false,I'll send the paperwork today.,1770735631000,call-9001,company,ACCT-204,renewed,phonecall-9002,call-9002-s1,true,We're comparing prices with another roaster.,1770921000000,call-9002,customer,ACCT-311,churn risk,phonecall-9002,call-9002-s2,false,What would it take to stay?,1770921001000,call-9002,company,ACCT-311,churn risk,phonecall-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. Aspeaker_rolecolumn sayscompanyorcustomer. - 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
sessionIdandconversationId, 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.
Script
Section titled “Script”Node 18 or later, with npm install papaparse.
// Turn call transcripts with named speakers into one row per turn.// Usage: node call-turns.mjs calls.csv call-turns.csvimport 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(', ')}`);node call-turns.mjs calls.csv call-turns.csv# 2 calls -> 8 turns# Treated as customers (check this list): Priya Shah, Jo ParkVariations
Section titled “Variations”- A participants column with emails: build
COMPANYfrom 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.