export type TranscriptItem = { speaker?: { source?: string; attribution?: string; diarization_label?: string; name?: string } text?: string start_time?: string end_time?: string } /** Human label for a transcript line: name, then diarization bucket, then me/them, then audio source. */ /** * Granola can append meeting-app badges to speaker names ("Jane Doe External unfamiliar", "Jane Doe Unverified"). * Strip those trailing badges for readable text; raw items keep the original name. */ export function cleanSpeakerName(name: string): string { let n = name.trim() for (let i = 0; i < 3; i++) { const next = n.replace(/\s+(?:external(?:\s+unfamiliar)?|unfamiliar|unverified|guest)$/i, '').trim() if (next === n || !next) break n = next } return n || name } export function speakerLabel(item: TranscriptItem): string { const s = item.speaker ?? {} if (s.name) return cleanSpeakerName(s.name) if (s.diarization_label) return s.diarization_label if (s.attribution === 'me') return 'Me' if (s.attribution === 'them') return 'Them' if (s.source === 'microphone') return 'Microphone' if (s.source === 'speaker') return 'Other participants' return 'Unknown' } function clock(iso: string | undefined, origin: number | null): string { if (!iso) return '' const t = Date.parse(iso) if (!Number.isFinite(t)) return iso if (origin === null) return iso const secs = Math.max(0, Math.round((t - origin) / 1000)) const h = Math.floor(secs / 3600) const m = Math.floor((secs % 3600) / 60) const s = secs % 60 return `${h ? `${h}:` : ''}${String(m).padStart(h ? 2 : 1, '0')}:${String(s).padStart(2, '0')}` } /** * Plain-text transcript: one line per item, `[m:ss] Speaker: text`, with consecutive lines by the * same speaker merged. Offsets are relative to the first item's start_time. */ export function transcriptToText(items: Array, opts: { timestamps?: boolean; merge?: boolean } = {}): string { const timestamps = opts.timestamps ?? true const merge = opts.merge ?? true const firstStart = items.find((i) => i.start_time && Number.isFinite(Date.parse(i.start_time)))?.start_time const origin = firstStart ? Date.parse(firstStart) : null const lines: Array<{ label: string; at: string; text: string }> = [] for (const item of items) { const text = (item.text ?? '').trim() if (!text) continue const label = speakerLabel(item) const last = lines[lines.length - 1] if (merge && last && last.label === label) last.text += ` ${text}` else lines.push({ label, at: clock(item.start_time, origin), text }) } return lines.map((l) => `${timestamps && l.at ? `[${l.at}] ` : ''}${l.label}: ${l.text}`).join('\n') }