Files
openclaw/src/transcripts/summary.ts
Peter Steinberger cac0b2db18 refactor: move transcripts into core
Move meeting notes into core transcripts, remove the bundled meeting-notes plugin/API, and require explicit transcripts.enabled before exposing the recording-capable tool.
2026-05-26 14:51:11 +01:00

97 lines
3.0 KiB
TypeScript

import { normalizeStringEntries } from "../shared/string-normalization.js";
import type { TranscriptSessionDescriptor, TranscriptUtterance } from "./provider-types.js";
export type TranscriptsSummary = {
sessionId: string;
title: string;
generatedAt: string;
overview: string;
transcript: string[];
decisions: string[];
actionItems: string[];
risks: string[];
utteranceCount: number;
};
const ACTION_PATTERNS =
/\b(todo|action|follow up|follow-up|assign|owner|next step|ship|fix|send|schedule)\b/i;
const DECISION_PATTERNS = /\b(decided|decision|we will|we'll|agreed|approved|go with|ship it)\b/i;
const RISK_PATTERNS =
/\b(risk|blocked|blocker|concern|issue|problem|unknown|deadline|privacy|security)\b/i;
function firstSentences(utterances: TranscriptUtterance[], limit: number): string {
const text = normalizeStringEntries(utterances.map((utterance) => utterance.text)).join(" ");
const sentences = text.match(/[^.!?]+[.!?]?/g) ?? [];
return normalizeStringEntries(sentences.slice(0, limit)).join(" ");
}
function collectMatches(utterances: TranscriptUtterance[], pattern: RegExp): string[] {
return utterances
.filter((utterance) => pattern.test(utterance.text))
.map(formatSpeakerLine)
.filter(Boolean)
.slice(0, 12);
}
function formatSpeakerLine(utterance: TranscriptUtterance): string {
const text = utterance.text.trim();
if (!text) {
return "";
}
const speaker = utterance.speaker?.label?.trim();
return speaker ? `${speaker}: ${text}` : text;
}
function formatTranscript(utterances: TranscriptUtterance[]): string[] {
return utterances.map(formatSpeakerLine).filter(Boolean);
}
export function summarizeTranscripts(params: {
session: TranscriptSessionDescriptor;
utterances: TranscriptUtterance[];
}): TranscriptsSummary {
const title = params.session.title?.trim() || "Transcripts";
const overview = firstSentences(params.utterances, 4) || "No transcript captured yet.";
return {
sessionId: params.session.sessionId,
title,
generatedAt: new Date().toISOString(),
overview,
transcript: formatTranscript(params.utterances),
decisions: collectMatches(params.utterances, DECISION_PATTERNS),
actionItems: collectMatches(params.utterances, ACTION_PATTERNS),
risks: collectMatches(params.utterances, RISK_PATTERNS),
utteranceCount: params.utterances.length,
};
}
function renderList(items: string[]): string {
return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None captured";
}
export function renderTranscriptsMarkdown(summary: TranscriptsSummary): string {
return [
`# ${summary.title}`,
"",
`Generated: ${summary.generatedAt}`,
`Session: ${summary.sessionId}`,
"",
"## Overview",
summary.overview,
"",
"## Transcript",
renderList(summary.transcript),
"",
"## Decisions",
renderList(summary.decisions),
"",
"## Action Items",
renderList(summary.actionItems),
"",
"## Risks",
renderList(summary.risks),
"",
`Transcript utterances: ${summary.utteranceCount}`,
].join("\n");
}