fireflies-api 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.cjs +1435 -217
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1436 -222
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +1372 -392
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +462 -3
- package/dist/index.d.ts +462 -3
- package/dist/index.js +1357 -393
- package/dist/index.js.map +1 -1
- package/dist/middleware/express.cjs +459 -16
- package/dist/middleware/express.cjs.map +1 -1
- package/dist/middleware/express.d.cts +2 -2
- package/dist/middleware/express.d.ts +2 -2
- package/dist/middleware/express.js +459 -16
- package/dist/middleware/express.js.map +1 -1
- package/dist/middleware/fastify.cjs +459 -16
- package/dist/middleware/fastify.cjs.map +1 -1
- package/dist/middleware/fastify.d.cts +2 -2
- package/dist/middleware/fastify.d.ts +2 -2
- package/dist/middleware/fastify.js +459 -16
- package/dist/middleware/fastify.js.map +1 -1
- package/dist/middleware/hono.cjs +459 -16
- package/dist/middleware/hono.cjs.map +1 -1
- package/dist/middleware/hono.d.cts +2 -2
- package/dist/middleware/hono.d.ts +2 -2
- package/dist/middleware/hono.js +459 -16
- package/dist/middleware/hono.js.map +1 -1
- package/dist/templates/digest/compact.md +8 -0
- package/dist/templates/digest/default.md +44 -0
- package/dist/templates/digest/executive.md +22 -0
- package/dist/{types-CaHcwnKw.d.ts → types-BMzVSd6w.d.ts} +1 -1
- package/dist/{types-BX-3JcRI.d.cts → types-BeXRmVD7.d.cts} +1 -1
- package/dist/{types-DIPZmUl3.d.ts → types-D2XsCR5R.d.ts} +120 -1
- package/dist/{types-C_XxdRd1.d.cts → types-zVGqyFzP.d.cts} +120 -1
- package/package.json +6 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { io } from 'socket.io-client';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
2
5
|
import { createHmac, timingSafeEqual } from 'crypto';
|
|
3
6
|
|
|
4
7
|
// src/errors.ts
|
|
@@ -596,6 +599,48 @@ function createUsersMutationsAPI(client) {
|
|
|
596
599
|
}
|
|
597
600
|
|
|
598
601
|
// src/helpers/pagination.ts
|
|
602
|
+
async function* paginateParallel(fetcher, options = {}) {
|
|
603
|
+
const { concurrency = 3, pageSize = 50, delayMs = 100 } = options;
|
|
604
|
+
const pending = /* @__PURE__ */ new Map();
|
|
605
|
+
let nextPageToFetch = 0;
|
|
606
|
+
let nextPageToYield = 0;
|
|
607
|
+
let foundEnd = false;
|
|
608
|
+
const delay4 = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
609
|
+
const startFetch = (pageIndex) => {
|
|
610
|
+
const skip = pageIndex * pageSize;
|
|
611
|
+
return fetcher(skip, pageSize);
|
|
612
|
+
};
|
|
613
|
+
const scheduleFetches = async () => {
|
|
614
|
+
while (!foundEnd && pending.size < concurrency) {
|
|
615
|
+
const pageIndex = nextPageToFetch++;
|
|
616
|
+
if (pageIndex > 0 && delayMs > 0) {
|
|
617
|
+
await delay4(delayMs);
|
|
618
|
+
}
|
|
619
|
+
if (foundEnd) break;
|
|
620
|
+
pending.set(pageIndex, startFetch(pageIndex));
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
pending.set(0, startFetch(0));
|
|
624
|
+
nextPageToFetch = 1;
|
|
625
|
+
while (pending.has(nextPageToYield)) {
|
|
626
|
+
const pagePromise = pending.get(nextPageToYield);
|
|
627
|
+
if (!pagePromise) break;
|
|
628
|
+
const page = await pagePromise;
|
|
629
|
+
pending.delete(nextPageToYield);
|
|
630
|
+
if (page.length < pageSize) {
|
|
631
|
+
foundEnd = true;
|
|
632
|
+
for (const item of page) {
|
|
633
|
+
yield item;
|
|
634
|
+
}
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
await scheduleFetches();
|
|
638
|
+
for (const item of page) {
|
|
639
|
+
yield item;
|
|
640
|
+
}
|
|
641
|
+
nextPageToYield++;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
599
644
|
async function* paginate(fetcher, pageSize = 50) {
|
|
600
645
|
let skip = 0;
|
|
601
646
|
let hasMore = true;
|
|
@@ -1255,6 +1300,62 @@ function getGroupKeyFn(groupBy2) {
|
|
|
1255
1300
|
}
|
|
1256
1301
|
}
|
|
1257
1302
|
|
|
1303
|
+
// src/helpers/batch.ts
|
|
1304
|
+
async function* batch(items, processor, options = {}) {
|
|
1305
|
+
const { delayMs = 100, handleRateLimit = true, maxRateLimitRetries = 3 } = options;
|
|
1306
|
+
let isFirst = true;
|
|
1307
|
+
for await (const item of items) {
|
|
1308
|
+
if (!isFirst && delayMs > 0) {
|
|
1309
|
+
await delay(delayMs);
|
|
1310
|
+
}
|
|
1311
|
+
isFirst = false;
|
|
1312
|
+
yield await processWithRetry(item, processor, {
|
|
1313
|
+
handleRateLimit,
|
|
1314
|
+
maxRateLimitRetries
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
async function batchAll(items, processor, options = {}) {
|
|
1319
|
+
const { continueOnError = false, ...batchOptions } = options;
|
|
1320
|
+
const results = [];
|
|
1321
|
+
const errors = [];
|
|
1322
|
+
for await (const batchResult of batch(items, processor, batchOptions)) {
|
|
1323
|
+
if (batchResult.error) {
|
|
1324
|
+
if (!continueOnError) {
|
|
1325
|
+
throw batchResult.error;
|
|
1326
|
+
}
|
|
1327
|
+
errors.push(batchResult.error);
|
|
1328
|
+
} else {
|
|
1329
|
+
results.push(batchResult.result);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return results;
|
|
1333
|
+
}
|
|
1334
|
+
async function processWithRetry(item, processor, options) {
|
|
1335
|
+
const { handleRateLimit, maxRateLimitRetries } = options;
|
|
1336
|
+
let retries = 0;
|
|
1337
|
+
while (true) {
|
|
1338
|
+
try {
|
|
1339
|
+
const result = await processor(item);
|
|
1340
|
+
return { item, result };
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
if (handleRateLimit && err instanceof RateLimitError && retries < maxRateLimitRetries) {
|
|
1343
|
+
const waitTime = err.retryAfter ?? 1e3;
|
|
1344
|
+
await delay(waitTime);
|
|
1345
|
+
retries++;
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
return {
|
|
1349
|
+
item,
|
|
1350
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
function delay(ms) {
|
|
1356
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1258
1359
|
// src/helpers/domain-utils.ts
|
|
1259
1360
|
function extractDomain(email) {
|
|
1260
1361
|
const atIndex = email.indexOf("@");
|
|
@@ -1270,134 +1371,518 @@ function hasExternalParticipants(participants, internalDomain) {
|
|
|
1270
1371
|
});
|
|
1271
1372
|
}
|
|
1272
1373
|
|
|
1273
|
-
// src/helpers/
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1374
|
+
// src/helpers/markdown.ts
|
|
1375
|
+
var DEFAULT_OPTIONS2 = {
|
|
1376
|
+
includeMetadata: true,
|
|
1377
|
+
includeSummary: true,
|
|
1378
|
+
includeActionItems: true,
|
|
1379
|
+
actionItemFormat: "checkbox",
|
|
1380
|
+
includeTimestamps: false,
|
|
1381
|
+
speakerFormat: "bold",
|
|
1382
|
+
groupBySpeaker: true
|
|
1383
|
+
};
|
|
1384
|
+
var DEFAULT_CHUNKS_OPTIONS = {
|
|
1385
|
+
title: "Live Transcript",
|
|
1386
|
+
includeTimestamps: false,
|
|
1387
|
+
speakerFormat: "bold",
|
|
1388
|
+
groupBySpeaker: true
|
|
1389
|
+
};
|
|
1390
|
+
async function transcriptToMarkdown(transcript, options = {}) {
|
|
1391
|
+
const opts = { ...DEFAULT_OPTIONS2, ...options };
|
|
1392
|
+
const sections = [];
|
|
1393
|
+
if (opts.includeMetadata) {
|
|
1394
|
+
sections.push(formatMetadata(transcript));
|
|
1278
1395
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
const totalUniqueSpeakers = speakerData.uniqueNames.size;
|
|
1289
|
-
const topSpeakers = buildTopSpeakers(speakerData.stats, topSpeakersCount);
|
|
1290
|
-
const { earliestMeeting, latestMeeting } = findDateRange(transcripts);
|
|
1291
|
-
return {
|
|
1292
|
-
totalMeetings: transcripts.length,
|
|
1293
|
-
totalDurationMinutes,
|
|
1294
|
-
averageDurationMinutes,
|
|
1295
|
-
byDayOfWeek,
|
|
1296
|
-
byTimeGroup,
|
|
1297
|
-
totalUniqueParticipants,
|
|
1298
|
-
averageParticipantsPerMeeting,
|
|
1299
|
-
topParticipants,
|
|
1300
|
-
totalUniqueSpeakers,
|
|
1301
|
-
topSpeakers,
|
|
1302
|
-
earliestMeeting,
|
|
1303
|
-
latestMeeting
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
function emptyInsights() {
|
|
1307
|
-
return {
|
|
1308
|
-
totalMeetings: 0,
|
|
1309
|
-
totalDurationMinutes: 0,
|
|
1310
|
-
averageDurationMinutes: 0,
|
|
1311
|
-
byDayOfWeek: emptyDayOfWeekStats(),
|
|
1312
|
-
byTimeGroup: void 0,
|
|
1313
|
-
totalUniqueParticipants: 0,
|
|
1314
|
-
averageParticipantsPerMeeting: 0,
|
|
1315
|
-
topParticipants: [],
|
|
1316
|
-
totalUniqueSpeakers: 0,
|
|
1317
|
-
topSpeakers: [],
|
|
1318
|
-
earliestMeeting: "",
|
|
1319
|
-
latestMeeting: ""
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1322
|
-
function emptyDayOfWeekStats() {
|
|
1323
|
-
const emptyDay = () => ({ count: 0, totalMinutes: 0 });
|
|
1324
|
-
return {
|
|
1325
|
-
monday: emptyDay(),
|
|
1326
|
-
tuesday: emptyDay(),
|
|
1327
|
-
wednesday: emptyDay(),
|
|
1328
|
-
thursday: emptyDay(),
|
|
1329
|
-
friday: emptyDay(),
|
|
1330
|
-
saturday: emptyDay(),
|
|
1331
|
-
sunday: emptyDay()
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
function sumDurations(transcripts) {
|
|
1335
|
-
return transcripts.reduce((sum, t) => sum + (t.duration ?? 0), 0);
|
|
1396
|
+
if (opts.includeSummary && transcript.summary) {
|
|
1397
|
+
sections.push(formatSummary(transcript.summary, opts));
|
|
1398
|
+
}
|
|
1399
|
+
if (transcript.sentences && transcript.sentences.length > 0) {
|
|
1400
|
+
sections.push(formatTranscript(transcript.sentences, opts));
|
|
1401
|
+
}
|
|
1402
|
+
const content = sections.join("\n\n---\n\n");
|
|
1403
|
+
await writeIfOutputPath(content, options.outputPath);
|
|
1404
|
+
return content;
|
|
1336
1405
|
}
|
|
1337
|
-
function
|
|
1338
|
-
const
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
"
|
|
1342
|
-
|
|
1343
|
-
"
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
if (dayName) {
|
|
1354
|
-
stats[dayName].count++;
|
|
1355
|
-
stats[dayName].totalMinutes += t.duration ?? 0;
|
|
1406
|
+
async function chunksToMarkdown(chunks, options = {}) {
|
|
1407
|
+
const opts = { ...DEFAULT_CHUNKS_OPTIONS, ...options };
|
|
1408
|
+
const lines = [`# ${opts.title}`];
|
|
1409
|
+
if (chunks.length === 0) {
|
|
1410
|
+
lines.push("", "## Transcript", "", "*No transcription data*");
|
|
1411
|
+
} else {
|
|
1412
|
+
lines.push("", "## Transcript");
|
|
1413
|
+
if (opts.groupBySpeaker) {
|
|
1414
|
+
const groups = groupChunksBySpeaker(chunks);
|
|
1415
|
+
for (const group of groups) {
|
|
1416
|
+
lines.push("", formatChunkGroup(group, opts));
|
|
1417
|
+
}
|
|
1418
|
+
} else {
|
|
1419
|
+
for (const chunk of chunks) {
|
|
1420
|
+
lines.push("", formatChunk(chunk, opts));
|
|
1421
|
+
}
|
|
1356
1422
|
}
|
|
1357
1423
|
}
|
|
1358
|
-
|
|
1424
|
+
const content = lines.join("\n");
|
|
1425
|
+
await writeIfOutputPath(content, options.outputPath);
|
|
1426
|
+
return content;
|
|
1359
1427
|
}
|
|
1360
|
-
function
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const period = formatPeriod(date, groupBy2);
|
|
1366
|
-
const existing = groups.get(period) ?? { count: 0, totalMinutes: 0 };
|
|
1367
|
-
existing.count++;
|
|
1368
|
-
existing.totalMinutes += t.duration ?? 0;
|
|
1369
|
-
groups.set(period, existing);
|
|
1428
|
+
function formatMetadata(transcript) {
|
|
1429
|
+
const lines = [`# ${transcript.title || "Untitled Meeting"}`];
|
|
1430
|
+
if (transcript.dateString) {
|
|
1431
|
+
lines.push(`
|
|
1432
|
+
**Date:** ${formatDate(transcript.dateString)}`);
|
|
1370
1433
|
}
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
period,
|
|
1375
|
-
count: data.count,
|
|
1376
|
-
totalMinutes: data.totalMinutes,
|
|
1377
|
-
averageMinutes: data.totalMinutes / data.count
|
|
1378
|
-
});
|
|
1434
|
+
const duration = calculateDuration(transcript);
|
|
1435
|
+
if (duration > 0) {
|
|
1436
|
+
lines.push(`**Duration:** ${formatDuration(duration)}`);
|
|
1379
1437
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1438
|
+
const participants = getParticipantNames(transcript);
|
|
1439
|
+
if (participants.length > 0) {
|
|
1440
|
+
lines.push(`**Participants:** ${participants.join(", ")}`);
|
|
1441
|
+
}
|
|
1442
|
+
return lines.join("\n");
|
|
1382
1443
|
}
|
|
1383
|
-
function
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
return `${year}-${month}-${day}`;
|
|
1390
|
-
case "week":
|
|
1391
|
-
return getISOWeek(date);
|
|
1392
|
-
case "month":
|
|
1393
|
-
return `${year}-${month}`;
|
|
1444
|
+
function calculateDuration(transcript) {
|
|
1445
|
+
if (transcript.sentences && transcript.sentences.length > 0) {
|
|
1446
|
+
const lastSentence = transcript.sentences[transcript.sentences.length - 1];
|
|
1447
|
+
if (lastSentence) {
|
|
1448
|
+
return parseFloat(lastSentence.end_time);
|
|
1449
|
+
}
|
|
1394
1450
|
}
|
|
1451
|
+
return transcript.duration || 0;
|
|
1395
1452
|
}
|
|
1396
|
-
function
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1453
|
+
function formatSummary(summary, opts) {
|
|
1454
|
+
const sections = ["## Summary"];
|
|
1455
|
+
if (summary.gist) {
|
|
1456
|
+
sections.push("", summary.gist);
|
|
1457
|
+
}
|
|
1458
|
+
if (summary.bullet_gist) {
|
|
1459
|
+
const bullets = parseMultilineField(summary.bullet_gist);
|
|
1460
|
+
if (bullets.length > 0) {
|
|
1461
|
+
sections.push("", "### Key Points");
|
|
1462
|
+
sections.push(bullets.map((p) => `- ${p}`).join("\n"));
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
if (opts.includeActionItems && summary.action_items) {
|
|
1466
|
+
const items = parseMultilineField(summary.action_items);
|
|
1467
|
+
if (items.length > 0) {
|
|
1468
|
+
sections.push("", "### Action Items");
|
|
1469
|
+
const prefix = opts.actionItemFormat === "checkbox" ? "- [ ] " : "- ";
|
|
1470
|
+
sections.push(items.map((a) => `${prefix}${a}`).join("\n"));
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return sections.join("\n");
|
|
1474
|
+
}
|
|
1475
|
+
function formatTranscript(sentences, opts) {
|
|
1476
|
+
const lines = ["## Transcript"];
|
|
1477
|
+
if (opts.groupBySpeaker) {
|
|
1478
|
+
const groups = groupSentencesBySpeaker(sentences);
|
|
1479
|
+
for (const group of groups) {
|
|
1480
|
+
lines.push("", formatSpeakerGroup(group, opts));
|
|
1481
|
+
}
|
|
1482
|
+
} else {
|
|
1483
|
+
for (const sentence of sentences) {
|
|
1484
|
+
lines.push("", formatSentence(sentence, opts));
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return lines.join("\n");
|
|
1488
|
+
}
|
|
1489
|
+
function groupSentencesBySpeaker(sentences) {
|
|
1490
|
+
const groups = [];
|
|
1491
|
+
let current = null;
|
|
1492
|
+
for (const sentence of sentences) {
|
|
1493
|
+
if (!current || current.speakerName !== sentence.speaker_name) {
|
|
1494
|
+
current = { speakerName: sentence.speaker_name, sentences: [] };
|
|
1495
|
+
groups.push(current);
|
|
1496
|
+
}
|
|
1497
|
+
current.sentences.push(sentence);
|
|
1498
|
+
}
|
|
1499
|
+
return groups;
|
|
1500
|
+
}
|
|
1501
|
+
function formatSpeakerGroup(group, opts) {
|
|
1502
|
+
const speaker = formatSpeakerName(group.speakerName, opts.speakerFormat);
|
|
1503
|
+
const text = group.sentences.map((s) => s.text).join(" ");
|
|
1504
|
+
const firstSentence = group.sentences[0];
|
|
1505
|
+
if (opts.includeTimestamps && firstSentence) {
|
|
1506
|
+
const timestamp = formatTimestamp(firstSentence.start_time);
|
|
1507
|
+
return `${timestamp} ${speaker} ${text}`;
|
|
1508
|
+
}
|
|
1509
|
+
return `${speaker} ${text}`;
|
|
1510
|
+
}
|
|
1511
|
+
function formatSentence(sentence, opts) {
|
|
1512
|
+
const speaker = formatSpeakerName(sentence.speaker_name, opts.speakerFormat);
|
|
1513
|
+
if (opts.includeTimestamps) {
|
|
1514
|
+
const timestamp = formatTimestamp(sentence.start_time);
|
|
1515
|
+
return `${timestamp} ${speaker} ${sentence.text}`;
|
|
1516
|
+
}
|
|
1517
|
+
return `${speaker} ${sentence.text}`;
|
|
1518
|
+
}
|
|
1519
|
+
function groupChunksBySpeaker(chunks) {
|
|
1520
|
+
const groups = [];
|
|
1521
|
+
let current = null;
|
|
1522
|
+
for (const chunk of chunks) {
|
|
1523
|
+
if (!current || current.speakerName !== chunk.speaker_name) {
|
|
1524
|
+
current = { speakerName: chunk.speaker_name, chunks: [] };
|
|
1525
|
+
groups.push(current);
|
|
1526
|
+
}
|
|
1527
|
+
current.chunks.push(chunk);
|
|
1528
|
+
}
|
|
1529
|
+
return groups;
|
|
1530
|
+
}
|
|
1531
|
+
function formatChunkGroup(group, opts) {
|
|
1532
|
+
const speaker = formatSpeakerName(group.speakerName, opts.speakerFormat);
|
|
1533
|
+
const text = group.chunks.map((c) => c.text).join(" ");
|
|
1534
|
+
const firstChunk = group.chunks[0];
|
|
1535
|
+
if (opts.includeTimestamps && firstChunk) {
|
|
1536
|
+
const timestamp = formatTimestamp(firstChunk.start_time.toString());
|
|
1537
|
+
return `${timestamp} ${speaker} ${text}`;
|
|
1538
|
+
}
|
|
1539
|
+
return `${speaker} ${text}`;
|
|
1540
|
+
}
|
|
1541
|
+
function formatChunk(chunk, opts) {
|
|
1542
|
+
const speaker = formatSpeakerName(chunk.speaker_name, opts.speakerFormat);
|
|
1543
|
+
if (opts.includeTimestamps) {
|
|
1544
|
+
const timestamp = formatTimestamp(chunk.start_time.toString());
|
|
1545
|
+
return `${timestamp} ${speaker} ${chunk.text}`;
|
|
1546
|
+
}
|
|
1547
|
+
return `${speaker} ${chunk.text}`;
|
|
1548
|
+
}
|
|
1549
|
+
function formatSpeakerName(name, format) {
|
|
1550
|
+
switch (format) {
|
|
1551
|
+
case "bold":
|
|
1552
|
+
return `**${name}:**`;
|
|
1553
|
+
case "plain":
|
|
1554
|
+
return `${name}:`;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
function formatTimestamp(startTime) {
|
|
1558
|
+
const seconds = parseFloat(startTime);
|
|
1559
|
+
const mins = Math.floor(seconds / 60);
|
|
1560
|
+
const secs = Math.floor(seconds % 60);
|
|
1561
|
+
return `[${mins}:${secs.toString().padStart(2, "0")}]`;
|
|
1562
|
+
}
|
|
1563
|
+
function formatDuration(seconds) {
|
|
1564
|
+
const hours = Math.floor(seconds / 3600);
|
|
1565
|
+
const mins = Math.floor(seconds % 3600 / 60);
|
|
1566
|
+
if (hours > 0) {
|
|
1567
|
+
return `${hours}h ${mins}m`;
|
|
1568
|
+
}
|
|
1569
|
+
return `${mins} minutes`;
|
|
1570
|
+
}
|
|
1571
|
+
function formatDate(isoString) {
|
|
1572
|
+
return new Date(isoString).toLocaleDateString("en-US", {
|
|
1573
|
+
weekday: "long",
|
|
1574
|
+
year: "numeric",
|
|
1575
|
+
month: "long",
|
|
1576
|
+
day: "numeric"
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
function getParticipantNames(transcript) {
|
|
1580
|
+
if (transcript.meeting_attendees?.length) {
|
|
1581
|
+
return transcript.meeting_attendees.map((a) => a.displayName || a.name || a.email).filter(Boolean);
|
|
1582
|
+
}
|
|
1583
|
+
return transcript.speakers?.map((s) => s.name) || [];
|
|
1584
|
+
}
|
|
1585
|
+
function parseMultilineField(value) {
|
|
1586
|
+
return value.split(/\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1587
|
+
}
|
|
1588
|
+
async function writeIfOutputPath(content, outputPath) {
|
|
1589
|
+
if (outputPath) {
|
|
1590
|
+
const { writeFile } = await import('fs/promises');
|
|
1591
|
+
await writeFile(outputPath, content, "utf-8");
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/helpers/export-formats.ts
|
|
1596
|
+
var DEFAULT_TEXT_OPTIONS = {
|
|
1597
|
+
includeTimestamps: false,
|
|
1598
|
+
includeMetadata: true
|
|
1599
|
+
};
|
|
1600
|
+
var DEFAULT_CSV_OPTIONS = {
|
|
1601
|
+
includeHeader: true,
|
|
1602
|
+
delimiter: ","
|
|
1603
|
+
};
|
|
1604
|
+
function transcriptToText(transcript, options = {}) {
|
|
1605
|
+
const opts = { ...DEFAULT_TEXT_OPTIONS, ...options };
|
|
1606
|
+
const sections = [];
|
|
1607
|
+
if (opts.includeMetadata) {
|
|
1608
|
+
sections.push(formatTextMetadata(transcript));
|
|
1609
|
+
}
|
|
1610
|
+
if (transcript.sentences && transcript.sentences.length > 0) {
|
|
1611
|
+
sections.push(formatTextTranscript(transcript.sentences, opts));
|
|
1612
|
+
}
|
|
1613
|
+
return sections.join("\n\n");
|
|
1614
|
+
}
|
|
1615
|
+
function transcriptToCsv(transcript, options = {}) {
|
|
1616
|
+
const opts = { ...DEFAULT_CSV_OPTIONS, ...options };
|
|
1617
|
+
const d = opts.delimiter;
|
|
1618
|
+
const lines = [];
|
|
1619
|
+
if (opts.includeHeader) {
|
|
1620
|
+
lines.push(`timestamp${d}speaker${d}text${d}is_question${d}is_task`);
|
|
1621
|
+
}
|
|
1622
|
+
for (const sentence of transcript.sentences) {
|
|
1623
|
+
const isQuestion = Boolean(sentence.ai_filters?.question);
|
|
1624
|
+
const isTask = Boolean(sentence.ai_filters?.task);
|
|
1625
|
+
const row = [
|
|
1626
|
+
sentence.start_time,
|
|
1627
|
+
escapeCsvField(sentence.speaker_name, d),
|
|
1628
|
+
escapeCsvField(sentence.text, d),
|
|
1629
|
+
String(isQuestion),
|
|
1630
|
+
String(isTask)
|
|
1631
|
+
];
|
|
1632
|
+
lines.push(row.join(d));
|
|
1633
|
+
}
|
|
1634
|
+
return lines.join("\n");
|
|
1635
|
+
}
|
|
1636
|
+
function sanitizeFilename(title) {
|
|
1637
|
+
if (!title.trim()) {
|
|
1638
|
+
return "untitled";
|
|
1639
|
+
}
|
|
1640
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 100);
|
|
1641
|
+
}
|
|
1642
|
+
function generateExportFilename(transcript, extension) {
|
|
1643
|
+
const sanitizedTitle = sanitizeFilename(transcript.title);
|
|
1644
|
+
let datePrefix = "";
|
|
1645
|
+
if (transcript.dateString) {
|
|
1646
|
+
try {
|
|
1647
|
+
const date = new Date(transcript.dateString);
|
|
1648
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1649
|
+
datePrefix = `${date.toISOString().slice(0, 10)}-`;
|
|
1650
|
+
}
|
|
1651
|
+
} catch {
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return `${datePrefix}${sanitizedTitle}.${extension}`;
|
|
1655
|
+
}
|
|
1656
|
+
async function exportTranscript(transcript, format) {
|
|
1657
|
+
switch (format) {
|
|
1658
|
+
case "markdown":
|
|
1659
|
+
return transcriptToMarkdown(transcript);
|
|
1660
|
+
case "json":
|
|
1661
|
+
return JSON.stringify(transcript, null, 2);
|
|
1662
|
+
case "txt":
|
|
1663
|
+
return transcriptToText(transcript);
|
|
1664
|
+
case "csv":
|
|
1665
|
+
return transcriptToCsv(transcript);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
async function createZipArchive(files) {
|
|
1669
|
+
const archiver = await import('archiver');
|
|
1670
|
+
const { Writable } = await import('stream');
|
|
1671
|
+
return new Promise((resolve, reject) => {
|
|
1672
|
+
const chunks = [];
|
|
1673
|
+
const archive = archiver.default("zip", { zlib: { level: 9 } });
|
|
1674
|
+
const writable = new Writable({
|
|
1675
|
+
write(chunk, _encoding, callback) {
|
|
1676
|
+
chunks.push(chunk);
|
|
1677
|
+
callback();
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
writable.on("finish", () => {
|
|
1681
|
+
resolve(Buffer.concat(chunks));
|
|
1682
|
+
});
|
|
1683
|
+
archive.on("error", reject);
|
|
1684
|
+
archive.pipe(writable);
|
|
1685
|
+
for (const file of files) {
|
|
1686
|
+
archive.append(file.content, { name: file.filename });
|
|
1687
|
+
}
|
|
1688
|
+
archive.finalize();
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
function formatTextMetadata(transcript) {
|
|
1692
|
+
const lines = [];
|
|
1693
|
+
lines.push(transcript.title || "Untitled Meeting");
|
|
1694
|
+
if (transcript.dateString) {
|
|
1695
|
+
lines.push(`Date: ${formatDate2(transcript.dateString)}`);
|
|
1696
|
+
}
|
|
1697
|
+
const participants = getParticipantNames2(transcript);
|
|
1698
|
+
if (participants.length > 0) {
|
|
1699
|
+
lines.push(`Participants: ${participants.join(", ")}`);
|
|
1700
|
+
}
|
|
1701
|
+
return lines.join("\n");
|
|
1702
|
+
}
|
|
1703
|
+
function formatTextTranscript(sentences, opts) {
|
|
1704
|
+
const groups = groupSentencesBySpeaker2(sentences);
|
|
1705
|
+
const lines = [];
|
|
1706
|
+
for (const group of groups) {
|
|
1707
|
+
const text = group.sentences.map((s) => s.text).join(" ");
|
|
1708
|
+
const speaker = group.speakerName || "Unknown";
|
|
1709
|
+
const firstSentence = group.sentences[0];
|
|
1710
|
+
if (opts.includeTimestamps && firstSentence) {
|
|
1711
|
+
const timestamp = formatTimestamp2(firstSentence.start_time);
|
|
1712
|
+
lines.push(`${timestamp} ${speaker}: ${text}`);
|
|
1713
|
+
} else {
|
|
1714
|
+
lines.push(`${speaker}: ${text}`);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return lines.join("\n");
|
|
1718
|
+
}
|
|
1719
|
+
function groupSentencesBySpeaker2(sentences) {
|
|
1720
|
+
const groups = [];
|
|
1721
|
+
let current = null;
|
|
1722
|
+
for (const sentence of sentences) {
|
|
1723
|
+
if (!current || current.speakerName !== sentence.speaker_name) {
|
|
1724
|
+
current = { speakerName: sentence.speaker_name, sentences: [] };
|
|
1725
|
+
groups.push(current);
|
|
1726
|
+
}
|
|
1727
|
+
current.sentences.push(sentence);
|
|
1728
|
+
}
|
|
1729
|
+
return groups;
|
|
1730
|
+
}
|
|
1731
|
+
function formatTimestamp2(startTime) {
|
|
1732
|
+
const seconds = parseFloat(startTime);
|
|
1733
|
+
const mins = Math.floor(seconds / 60);
|
|
1734
|
+
const secs = Math.floor(seconds % 60);
|
|
1735
|
+
return `[${mins}:${secs.toString().padStart(2, "0")}]`;
|
|
1736
|
+
}
|
|
1737
|
+
function formatDate2(isoString) {
|
|
1738
|
+
return new Date(isoString).toLocaleDateString("en-US", {
|
|
1739
|
+
weekday: "long",
|
|
1740
|
+
year: "numeric",
|
|
1741
|
+
month: "long",
|
|
1742
|
+
day: "numeric"
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
function getParticipantNames2(transcript) {
|
|
1746
|
+
if (transcript.meeting_attendees?.length) {
|
|
1747
|
+
return transcript.meeting_attendees.map((a) => a.displayName || a.name || a.email).filter(Boolean);
|
|
1748
|
+
}
|
|
1749
|
+
return transcript.speakers?.map((s) => s.name) || [];
|
|
1750
|
+
}
|
|
1751
|
+
function escapeCsvField(field, delimiter) {
|
|
1752
|
+
if (field.includes('"') || field.includes(delimiter) || field.includes("\n")) {
|
|
1753
|
+
return `"${field.replace(/"/g, '""')}"`;
|
|
1754
|
+
}
|
|
1755
|
+
return field;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// src/helpers/meeting-insights.ts
|
|
1759
|
+
function analyzeMeetings(transcripts, options = {}) {
|
|
1760
|
+
const { speakers, groupBy: groupBy2, topSpeakersCount = 10, topParticipantsCount = 10 } = options;
|
|
1761
|
+
if (transcripts.length === 0) {
|
|
1762
|
+
return emptyInsights();
|
|
1763
|
+
}
|
|
1764
|
+
const totalDurationMinutes = sumDurations(transcripts);
|
|
1765
|
+
const averageDurationMinutes = totalDurationMinutes / transcripts.length;
|
|
1766
|
+
const byDayOfWeek = calculateDayOfWeekStats(transcripts);
|
|
1767
|
+
const byTimeGroup = groupBy2 ? calculateTimeGroupStats(transcripts, groupBy2) : void 0;
|
|
1768
|
+
const participantData = aggregateParticipants(transcripts);
|
|
1769
|
+
const totalUniqueParticipants = participantData.uniqueEmails.size;
|
|
1770
|
+
const averageParticipantsPerMeeting = calculateAverageParticipants(transcripts);
|
|
1771
|
+
const topParticipants = buildTopParticipants(participantData.stats, topParticipantsCount);
|
|
1772
|
+
const speakerData = aggregateSpeakers(transcripts, speakers);
|
|
1773
|
+
const totalUniqueSpeakers = speakerData.uniqueNames.size;
|
|
1774
|
+
const topSpeakers = buildTopSpeakers(speakerData.stats, topSpeakersCount);
|
|
1775
|
+
const { earliestMeeting, latestMeeting } = findDateRange(transcripts);
|
|
1776
|
+
return {
|
|
1777
|
+
totalMeetings: transcripts.length,
|
|
1778
|
+
totalDurationMinutes,
|
|
1779
|
+
averageDurationMinutes,
|
|
1780
|
+
byDayOfWeek,
|
|
1781
|
+
byTimeGroup,
|
|
1782
|
+
totalUniqueParticipants,
|
|
1783
|
+
averageParticipantsPerMeeting,
|
|
1784
|
+
topParticipants,
|
|
1785
|
+
totalUniqueSpeakers,
|
|
1786
|
+
topSpeakers,
|
|
1787
|
+
earliestMeeting,
|
|
1788
|
+
latestMeeting
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
function emptyInsights() {
|
|
1792
|
+
return {
|
|
1793
|
+
totalMeetings: 0,
|
|
1794
|
+
totalDurationMinutes: 0,
|
|
1795
|
+
averageDurationMinutes: 0,
|
|
1796
|
+
byDayOfWeek: emptyDayOfWeekStats(),
|
|
1797
|
+
byTimeGroup: void 0,
|
|
1798
|
+
totalUniqueParticipants: 0,
|
|
1799
|
+
averageParticipantsPerMeeting: 0,
|
|
1800
|
+
topParticipants: [],
|
|
1801
|
+
totalUniqueSpeakers: 0,
|
|
1802
|
+
topSpeakers: [],
|
|
1803
|
+
earliestMeeting: "",
|
|
1804
|
+
latestMeeting: ""
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
function emptyDayOfWeekStats() {
|
|
1808
|
+
const emptyDay = () => ({ count: 0, totalMinutes: 0 });
|
|
1809
|
+
return {
|
|
1810
|
+
monday: emptyDay(),
|
|
1811
|
+
tuesday: emptyDay(),
|
|
1812
|
+
wednesday: emptyDay(),
|
|
1813
|
+
thursday: emptyDay(),
|
|
1814
|
+
friday: emptyDay(),
|
|
1815
|
+
saturday: emptyDay(),
|
|
1816
|
+
sunday: emptyDay()
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
function sumDurations(transcripts) {
|
|
1820
|
+
return transcripts.reduce((sum, t) => sum + (t.duration ?? 0), 0);
|
|
1821
|
+
}
|
|
1822
|
+
function calculateDayOfWeekStats(transcripts) {
|
|
1823
|
+
const stats = emptyDayOfWeekStats();
|
|
1824
|
+
const dayNames = [
|
|
1825
|
+
"sunday",
|
|
1826
|
+
"monday",
|
|
1827
|
+
"tuesday",
|
|
1828
|
+
"wednesday",
|
|
1829
|
+
"thursday",
|
|
1830
|
+
"friday",
|
|
1831
|
+
"saturday"
|
|
1832
|
+
];
|
|
1833
|
+
for (const t of transcripts) {
|
|
1834
|
+
const date = parseDate(t.dateString);
|
|
1835
|
+
if (!date) continue;
|
|
1836
|
+
const dayIndex = date.getUTCDay();
|
|
1837
|
+
const dayName = dayNames[dayIndex];
|
|
1838
|
+
if (dayName) {
|
|
1839
|
+
stats[dayName].count++;
|
|
1840
|
+
stats[dayName].totalMinutes += t.duration ?? 0;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
return stats;
|
|
1844
|
+
}
|
|
1845
|
+
function calculateTimeGroupStats(transcripts, groupBy2) {
|
|
1846
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1847
|
+
for (const t of transcripts) {
|
|
1848
|
+
const date = parseDate(t.dateString);
|
|
1849
|
+
if (!date) continue;
|
|
1850
|
+
const period = formatPeriod(date, groupBy2);
|
|
1851
|
+
const existing = groups.get(period) ?? { count: 0, totalMinutes: 0 };
|
|
1852
|
+
existing.count++;
|
|
1853
|
+
existing.totalMinutes += t.duration ?? 0;
|
|
1854
|
+
groups.set(period, existing);
|
|
1855
|
+
}
|
|
1856
|
+
const result = [];
|
|
1857
|
+
for (const [period, data] of groups) {
|
|
1858
|
+
result.push({
|
|
1859
|
+
period,
|
|
1860
|
+
count: data.count,
|
|
1861
|
+
totalMinutes: data.totalMinutes,
|
|
1862
|
+
averageMinutes: data.totalMinutes / data.count
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
result.sort((a, b) => a.period.localeCompare(b.period));
|
|
1866
|
+
return result;
|
|
1867
|
+
}
|
|
1868
|
+
function formatPeriod(date, groupBy2) {
|
|
1869
|
+
const year = date.getUTCFullYear();
|
|
1870
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
1871
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
1872
|
+
switch (groupBy2) {
|
|
1873
|
+
case "day":
|
|
1874
|
+
return `${year}-${month}-${day}`;
|
|
1875
|
+
case "week":
|
|
1876
|
+
return getISOWeek(date);
|
|
1877
|
+
case "month":
|
|
1878
|
+
return `${year}-${month}`;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
function getISOWeek(date) {
|
|
1882
|
+
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
1883
|
+
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
|
|
1884
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
1885
|
+
const weekNumber = Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
|
|
1401
1886
|
return `${d.getUTCFullYear()}-W${String(weekNumber).padStart(2, "0")}`;
|
|
1402
1887
|
}
|
|
1403
1888
|
function aggregateParticipants(transcripts) {
|
|
@@ -1743,6 +2228,21 @@ var TRANSCRIPT_LIST_FIELDS = `
|
|
|
1743
2228
|
summary_status
|
|
1744
2229
|
}
|
|
1745
2230
|
`;
|
|
2231
|
+
function buildListFields(params) {
|
|
2232
|
+
const includeSentences = params?.includeSentences === true;
|
|
2233
|
+
const includeSummary = params?.includeSummary === true;
|
|
2234
|
+
if (!includeSentences && !includeSummary) {
|
|
2235
|
+
return TRANSCRIPT_LIST_FIELDS;
|
|
2236
|
+
}
|
|
2237
|
+
let fields = TRANSCRIPT_BASE_FIELDS;
|
|
2238
|
+
if (includeSentences) {
|
|
2239
|
+
fields += SENTENCES_FIELDS;
|
|
2240
|
+
}
|
|
2241
|
+
if (includeSummary) {
|
|
2242
|
+
fields += SUMMARY_FIELDS;
|
|
2243
|
+
}
|
|
2244
|
+
return fields;
|
|
2245
|
+
}
|
|
1746
2246
|
function createTranscriptsAPI(client) {
|
|
1747
2247
|
return {
|
|
1748
2248
|
async get(id, params) {
|
|
@@ -1758,6 +2258,7 @@ function createTranscriptsAPI(client) {
|
|
|
1758
2258
|
return normalizeTranscript(data.transcript);
|
|
1759
2259
|
},
|
|
1760
2260
|
async list(params) {
|
|
2261
|
+
const fields = buildListFields(params);
|
|
1761
2262
|
const query = `
|
|
1762
2263
|
query ListTranscripts(
|
|
1763
2264
|
$keyword: String
|
|
@@ -1795,13 +2296,23 @@ function createTranscriptsAPI(client) {
|
|
|
1795
2296
|
participant_email: $participant_email
|
|
1796
2297
|
date: $date
|
|
1797
2298
|
) {
|
|
1798
|
-
${
|
|
2299
|
+
${fields}
|
|
1799
2300
|
}
|
|
1800
2301
|
}
|
|
1801
2302
|
`;
|
|
2303
|
+
let internalDomain;
|
|
2304
|
+
if (params?.external) {
|
|
2305
|
+
const userQuery = "query { user { email } }";
|
|
2306
|
+
const userData = await client.execute(userQuery);
|
|
2307
|
+
internalDomain = extractDomain(userData.user.email);
|
|
2308
|
+
}
|
|
1802
2309
|
const variables = buildListVariables(params);
|
|
1803
2310
|
const data = await client.execute(query, variables);
|
|
1804
|
-
|
|
2311
|
+
let results = data.transcripts.map(normalizeTranscript);
|
|
2312
|
+
if (internalDomain) {
|
|
2313
|
+
results = results.filter((t) => hasExternalParticipants(t.participants, internalDomain));
|
|
2314
|
+
}
|
|
2315
|
+
return results;
|
|
1805
2316
|
},
|
|
1806
2317
|
async getSummary(id) {
|
|
1807
2318
|
const query = `
|
|
@@ -1848,9 +2359,10 @@ function createTranscriptsAPI(client) {
|
|
|
1848
2359
|
} = params;
|
|
1849
2360
|
const transcripts = [];
|
|
1850
2361
|
for await (const t of this.listAll({
|
|
2362
|
+
...listParams,
|
|
1851
2363
|
keyword: query,
|
|
1852
2364
|
scope,
|
|
1853
|
-
|
|
2365
|
+
includeSentences: true
|
|
1854
2366
|
})) {
|
|
1855
2367
|
transcripts.push(t);
|
|
1856
2368
|
if (limit && transcripts.length >= limit) break;
|
|
@@ -1858,8 +2370,7 @@ function createTranscriptsAPI(client) {
|
|
|
1858
2370
|
const allMatches = [];
|
|
1859
2371
|
let transcriptsWithMatches = 0;
|
|
1860
2372
|
for (const t of transcripts) {
|
|
1861
|
-
const
|
|
1862
|
-
const matches = searchTranscript(full, {
|
|
2373
|
+
const matches = searchTranscript(t, {
|
|
1863
2374
|
query,
|
|
1864
2375
|
caseSensitive,
|
|
1865
2376
|
speakers,
|
|
@@ -1911,13 +2422,13 @@ function createTranscriptsAPI(client) {
|
|
|
1911
2422
|
organizers,
|
|
1912
2423
|
participants,
|
|
1913
2424
|
user_id,
|
|
1914
|
-
channel_id
|
|
2425
|
+
channel_id,
|
|
2426
|
+
includeSentences: true
|
|
1915
2427
|
})) {
|
|
1916
2428
|
if (internalDomain && !hasExternalParticipants(t.participants, internalDomain)) {
|
|
1917
2429
|
continue;
|
|
1918
2430
|
}
|
|
1919
|
-
|
|
1920
|
-
transcripts.push(full);
|
|
2431
|
+
transcripts.push(t);
|
|
1921
2432
|
if (limit && transcripts.length >= limit) break;
|
|
1922
2433
|
}
|
|
1923
2434
|
return analyzeMeetings(transcripts, {
|
|
@@ -1935,13 +2446,20 @@ function createTranscriptsAPI(client) {
|
|
|
1935
2446
|
toDate,
|
|
1936
2447
|
mine,
|
|
1937
2448
|
organizers,
|
|
1938
|
-
participants
|
|
2449
|
+
participants,
|
|
2450
|
+
includeSummary: true
|
|
1939
2451
|
})) {
|
|
1940
|
-
|
|
1941
|
-
transcripts.push(full);
|
|
2452
|
+
transcripts.push(t);
|
|
1942
2453
|
if (limit && transcripts.length >= limit) break;
|
|
1943
2454
|
}
|
|
1944
2455
|
return aggregateActionItems(transcripts, {}, filterOptions);
|
|
2456
|
+
},
|
|
2457
|
+
async bulkExport(params = {}) {
|
|
2458
|
+
const { format = "markdown", asZip = false, onProgress } = params;
|
|
2459
|
+
const transcriptIds = await collectTranscriptIds(client, this, params);
|
|
2460
|
+
const files = await convertTranscripts(this, transcriptIds, format, onProgress);
|
|
2461
|
+
const zip = asZip ? await createZipArchive(files) : void 0;
|
|
2462
|
+
return { files, zip, format, totalExported: files.length };
|
|
1945
2463
|
}
|
|
1946
2464
|
};
|
|
1947
2465
|
}
|
|
@@ -2020,6 +2538,50 @@ function buildListVariables(params) {
|
|
|
2020
2538
|
date: params.date
|
|
2021
2539
|
};
|
|
2022
2540
|
}
|
|
2541
|
+
async function collectTranscriptIds(client, api, params) {
|
|
2542
|
+
const { ids, fromDate, toDate, mine, organizers, participants, external, limit } = params;
|
|
2543
|
+
if (ids?.length) {
|
|
2544
|
+
return ids;
|
|
2545
|
+
}
|
|
2546
|
+
let internalDomain;
|
|
2547
|
+
if (external) {
|
|
2548
|
+
const userQuery = "query { user { email } }";
|
|
2549
|
+
const userData = await client.execute(userQuery);
|
|
2550
|
+
internalDomain = extractDomain(userData.user.email);
|
|
2551
|
+
}
|
|
2552
|
+
const result = [];
|
|
2553
|
+
for await (const t of api.listAll({ fromDate, toDate, mine, organizers, participants })) {
|
|
2554
|
+
if (internalDomain && !hasExternalParticipants(t.participants, internalDomain)) {
|
|
2555
|
+
continue;
|
|
2556
|
+
}
|
|
2557
|
+
result.push(t.id);
|
|
2558
|
+
if (limit && result.length >= limit) break;
|
|
2559
|
+
}
|
|
2560
|
+
return result;
|
|
2561
|
+
}
|
|
2562
|
+
async function convertTranscripts(api, transcriptIds, format, onProgress) {
|
|
2563
|
+
const files = [];
|
|
2564
|
+
const extension = format === "markdown" ? "md" : format;
|
|
2565
|
+
let completed = 0;
|
|
2566
|
+
const total = transcriptIds.length;
|
|
2567
|
+
for await (const result of batch(
|
|
2568
|
+
transcriptIds,
|
|
2569
|
+
async (id) => {
|
|
2570
|
+
const transcript = await api.get(id);
|
|
2571
|
+
const content = await exportTranscript(transcript, format);
|
|
2572
|
+
const filename = generateExportFilename(transcript, extension);
|
|
2573
|
+
return { id, title: transcript.title, filename, content };
|
|
2574
|
+
},
|
|
2575
|
+
{ delayMs: 100 }
|
|
2576
|
+
)) {
|
|
2577
|
+
if (!result.error) {
|
|
2578
|
+
files.push(result.result);
|
|
2579
|
+
}
|
|
2580
|
+
completed++;
|
|
2581
|
+
onProgress?.(completed, total);
|
|
2582
|
+
}
|
|
2583
|
+
return files;
|
|
2584
|
+
}
|
|
2023
2585
|
|
|
2024
2586
|
// src/graphql/queries/users.ts
|
|
2025
2587
|
var USER_FIELDS = `
|
|
@@ -2558,61 +3120,684 @@ var TranscriptAccumulator = class {
|
|
|
2558
3120
|
}
|
|
2559
3121
|
};
|
|
2560
3122
|
|
|
2561
|
-
// src/helpers/
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
3123
|
+
// src/helpers/digest.ts
|
|
3124
|
+
var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
3125
|
+
function calculateStats(transcripts) {
|
|
3126
|
+
if (transcripts.length === 0) {
|
|
3127
|
+
return emptyStats();
|
|
3128
|
+
}
|
|
3129
|
+
const totalMinutes = sumDurations2(transcripts);
|
|
3130
|
+
const meetingsByDay = calculateMeetingsByDay(transcripts);
|
|
3131
|
+
const busiestDay = findBusiestDay(meetingsByDay);
|
|
3132
|
+
return {
|
|
3133
|
+
totalMeetings: transcripts.length,
|
|
3134
|
+
totalMinutes,
|
|
3135
|
+
averageDuration: totalMinutes / transcripts.length,
|
|
3136
|
+
busiestDay,
|
|
3137
|
+
meetingsByDay
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
function emptyStats() {
|
|
3141
|
+
return {
|
|
3142
|
+
totalMeetings: 0,
|
|
3143
|
+
totalMinutes: 0,
|
|
3144
|
+
averageDuration: 0,
|
|
3145
|
+
busiestDay: "",
|
|
3146
|
+
meetingsByDay: {}
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
function sumDurations2(transcripts) {
|
|
3150
|
+
return transcripts.reduce((sum, t) => sum + (t.duration ?? 0), 0);
|
|
3151
|
+
}
|
|
3152
|
+
function calculateMeetingsByDay(transcripts) {
|
|
3153
|
+
const counts = {};
|
|
3154
|
+
for (const t of transcripts) {
|
|
3155
|
+
const date = parseDate2(t.dateString);
|
|
3156
|
+
if (!date) continue;
|
|
3157
|
+
const dayName = DAY_NAMES[date.getUTCDay()];
|
|
3158
|
+
if (dayName) {
|
|
3159
|
+
counts[dayName] = (counts[dayName] ?? 0) + 1;
|
|
2568
3160
|
}
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
3161
|
+
}
|
|
3162
|
+
return counts;
|
|
3163
|
+
}
|
|
3164
|
+
function findBusiestDay(meetingsByDay) {
|
|
3165
|
+
let busiestDay = "";
|
|
3166
|
+
let maxCount = 0;
|
|
3167
|
+
for (const [day, count] of Object.entries(meetingsByDay)) {
|
|
3168
|
+
if (count > maxCount) {
|
|
3169
|
+
maxCount = count;
|
|
3170
|
+
busiestDay = day;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
return busiestDay;
|
|
3174
|
+
}
|
|
3175
|
+
function parseDate2(dateString) {
|
|
3176
|
+
if (!dateString) return null;
|
|
3177
|
+
const date = new Date(dateString);
|
|
3178
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
3179
|
+
}
|
|
3180
|
+
var MAX_KEY_POINTS_PER_MEETING = 5;
|
|
3181
|
+
function extractHighlights(transcripts) {
|
|
3182
|
+
const highlights = [];
|
|
3183
|
+
for (const t of transcripts) {
|
|
3184
|
+
const overview = t.summary?.overview;
|
|
3185
|
+
if (!overview || overview.trim().length === 0) continue;
|
|
3186
|
+
const keyPoints = extractKeyPoints(overview);
|
|
3187
|
+
if (keyPoints.length === 0) continue;
|
|
3188
|
+
highlights.push({
|
|
3189
|
+
meetingId: t.id,
|
|
3190
|
+
meetingTitle: t.title,
|
|
3191
|
+
meetingDate: t.dateString,
|
|
3192
|
+
keyPoints,
|
|
3193
|
+
decisions: extractDecisions(t)
|
|
2573
3194
|
});
|
|
2574
3195
|
}
|
|
3196
|
+
return highlights;
|
|
3197
|
+
}
|
|
3198
|
+
function extractKeyPoints(overview) {
|
|
3199
|
+
const sentences = overview.split(/(?<=[.!?])\s+/).map((s) => s.trim().replace(/[.!?]$/, "")).map((s) => s.replace(/^-\s*/, "")).filter((s) => s.length > 0);
|
|
3200
|
+
return sentences.slice(0, MAX_KEY_POINTS_PER_MEETING);
|
|
3201
|
+
}
|
|
3202
|
+
function extractDecisions(transcript) {
|
|
3203
|
+
const decisions = [];
|
|
3204
|
+
const overview = transcript.summary?.overview ?? "";
|
|
3205
|
+
const decisionPattern = /(?:^|[.!?]\s*)([^.!?]*(?:decided?|decision|agreed|approved)[^.!?]*)/gi;
|
|
3206
|
+
for (; ; ) {
|
|
3207
|
+
const match = decisionPattern.exec(overview);
|
|
3208
|
+
if (!match) break;
|
|
3209
|
+
if (match[1]) {
|
|
3210
|
+
decisions.push(match[1].trim());
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
return decisions;
|
|
2575
3214
|
}
|
|
2576
|
-
|
|
2577
|
-
const
|
|
2578
|
-
const
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
3215
|
+
function buildNameLookup(transcripts) {
|
|
3216
|
+
const namesByEmail = /* @__PURE__ */ new Map();
|
|
3217
|
+
for (const t of transcripts) {
|
|
3218
|
+
for (const attendee of t.meeting_attendees ?? []) {
|
|
3219
|
+
if (!attendee.email) continue;
|
|
3220
|
+
const normalizedEmail = attendee.email.toLowerCase();
|
|
3221
|
+
const name = attendee.displayName || attendee.name;
|
|
3222
|
+
if (name && !namesByEmail.has(normalizedEmail)) {
|
|
3223
|
+
namesByEmail.set(normalizedEmail, name);
|
|
2584
3224
|
}
|
|
2585
|
-
errors.push(batchResult.error);
|
|
2586
|
-
} else {
|
|
2587
|
-
results.push(batchResult.result);
|
|
2588
3225
|
}
|
|
2589
3226
|
}
|
|
2590
|
-
return
|
|
3227
|
+
return namesByEmail;
|
|
2591
3228
|
}
|
|
2592
|
-
|
|
2593
|
-
const
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
const
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
3229
|
+
function countParticipation(transcripts, namesByEmail) {
|
|
3230
|
+
const stats = /* @__PURE__ */ new Map();
|
|
3231
|
+
for (const t of transcripts) {
|
|
3232
|
+
const seenInMeeting = /* @__PURE__ */ new Set();
|
|
3233
|
+
for (const email of t.participants ?? []) {
|
|
3234
|
+
const normalizedEmail = email.toLowerCase();
|
|
3235
|
+
if (seenInMeeting.has(normalizedEmail)) continue;
|
|
3236
|
+
seenInMeeting.add(normalizedEmail);
|
|
3237
|
+
const existing = stats.get(normalizedEmail) ?? {
|
|
3238
|
+
name: namesByEmail.get(normalizedEmail) || extractNameFromEmail(email),
|
|
3239
|
+
meetingCount: 0,
|
|
3240
|
+
totalMinutes: 0
|
|
3241
|
+
};
|
|
3242
|
+
existing.meetingCount++;
|
|
3243
|
+
existing.totalMinutes += t.duration ?? 0;
|
|
3244
|
+
stats.set(normalizedEmail, existing);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
return stats;
|
|
3248
|
+
}
|
|
3249
|
+
function aggregateParticipants2(transcripts) {
|
|
3250
|
+
const namesByEmail = buildNameLookup(transcripts);
|
|
3251
|
+
const stats = countParticipation(transcripts, namesByEmail);
|
|
3252
|
+
return Array.from(stats, ([email, data]) => ({
|
|
3253
|
+
email,
|
|
3254
|
+
name: data.name,
|
|
3255
|
+
meetingCount: data.meetingCount,
|
|
3256
|
+
totalMinutes: data.totalMinutes
|
|
3257
|
+
})).sort((a, b) => b.meetingCount - a.meetingCount);
|
|
3258
|
+
}
|
|
3259
|
+
function extractNameFromEmail(email) {
|
|
3260
|
+
const localPart = email.split("@")[0] ?? email;
|
|
3261
|
+
return localPart;
|
|
3262
|
+
}
|
|
3263
|
+
function buildParticipantInfoList(emails, attendees) {
|
|
3264
|
+
const attendeeMap = /* @__PURE__ */ new Map();
|
|
3265
|
+
for (const attendee of attendees) {
|
|
3266
|
+
if (attendee.email) {
|
|
3267
|
+
attendeeMap.set(attendee.email.toLowerCase(), attendee);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
return emails.map((email) => {
|
|
3271
|
+
const normalizedEmail = email.toLowerCase();
|
|
3272
|
+
const attendee = attendeeMap.get(normalizedEmail);
|
|
3273
|
+
return {
|
|
3274
|
+
email: normalizedEmail,
|
|
3275
|
+
name: attendee?.displayName || attendee?.name || extractNameFromEmail(normalizedEmail)
|
|
3276
|
+
};
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
function aggregateActionItemsForDigest(transcripts) {
|
|
3280
|
+
if (transcripts.length === 0) {
|
|
3281
|
+
return emptyActionItems();
|
|
3282
|
+
}
|
|
3283
|
+
const byAssignee = {};
|
|
3284
|
+
const byMeeting = [];
|
|
3285
|
+
const unassigned = [];
|
|
3286
|
+
const withDueDates = [];
|
|
3287
|
+
let total = 0;
|
|
3288
|
+
for (const t of transcripts) {
|
|
3289
|
+
const result = extractActionItems(t);
|
|
3290
|
+
const meetingItems = [];
|
|
3291
|
+
for (const item of result.items) {
|
|
3292
|
+
const digestItem = {
|
|
3293
|
+
...item,
|
|
3294
|
+
transcriptId: t.id,
|
|
3295
|
+
transcriptTitle: t.title,
|
|
3296
|
+
transcriptDate: t.dateString
|
|
2609
3297
|
};
|
|
3298
|
+
total++;
|
|
3299
|
+
meetingItems.push(digestItem);
|
|
3300
|
+
if (item.assignee) {
|
|
3301
|
+
const existing = byAssignee[item.assignee] ?? [];
|
|
3302
|
+
existing.push(digestItem);
|
|
3303
|
+
byAssignee[item.assignee] = existing;
|
|
3304
|
+
} else {
|
|
3305
|
+
unassigned.push(digestItem);
|
|
3306
|
+
}
|
|
3307
|
+
if (item.dueDate) {
|
|
3308
|
+
withDueDates.push(digestItem);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
if (meetingItems.length > 0) {
|
|
3312
|
+
byMeeting.push({
|
|
3313
|
+
id: t.id,
|
|
3314
|
+
title: t.title,
|
|
3315
|
+
date: t.dateString,
|
|
3316
|
+
duration: t.duration ?? 0,
|
|
3317
|
+
participants: buildParticipantInfoList(t.participants ?? [], t.meeting_attendees ?? []),
|
|
3318
|
+
items: meetingItems
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
return { total, byAssignee, byMeeting, unassigned, withDueDates };
|
|
3323
|
+
}
|
|
3324
|
+
function emptyActionItems() {
|
|
3325
|
+
return {
|
|
3326
|
+
total: 0,
|
|
3327
|
+
byAssignee: {},
|
|
3328
|
+
byMeeting: [],
|
|
3329
|
+
unassigned: [],
|
|
3330
|
+
withDueDates: []
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
function buildDigest(transcripts, options = {}) {
|
|
3334
|
+
const { includeActionItems = true, includeHighlights = true, includeStats = true } = options;
|
|
3335
|
+
if (transcripts.length === 0) {
|
|
3336
|
+
return emptyDigest();
|
|
3337
|
+
}
|
|
3338
|
+
const stats = includeStats ? calculateStats(transcripts) : emptyStats();
|
|
3339
|
+
const actionItems = includeActionItems ? aggregateActionItemsForDigest(transcripts) : emptyActionItems();
|
|
3340
|
+
const highlights = includeHighlights ? extractHighlights(transcripts) : [];
|
|
3341
|
+
const participants = aggregateParticipants2(transcripts);
|
|
3342
|
+
const meetings = transcripts.map(toMeetingSummary);
|
|
3343
|
+
const period = calculatePeriod(transcripts);
|
|
3344
|
+
const totalDuration = sumDurations2(transcripts);
|
|
3345
|
+
return {
|
|
3346
|
+
period,
|
|
3347
|
+
totalMeetings: transcripts.length,
|
|
3348
|
+
totalDuration,
|
|
3349
|
+
stats,
|
|
3350
|
+
actionItems,
|
|
3351
|
+
highlights,
|
|
3352
|
+
participants,
|
|
3353
|
+
meetings
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
function emptyDigest() {
|
|
3357
|
+
return {
|
|
3358
|
+
period: { from: "", to: "" },
|
|
3359
|
+
totalMeetings: 0,
|
|
3360
|
+
totalDuration: 0,
|
|
3361
|
+
stats: emptyStats(),
|
|
3362
|
+
actionItems: emptyActionItems(),
|
|
3363
|
+
highlights: [],
|
|
3364
|
+
participants: [],
|
|
3365
|
+
meetings: []
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
function toMeetingSummary(transcript) {
|
|
3369
|
+
return {
|
|
3370
|
+
id: transcript.id,
|
|
3371
|
+
title: transcript.title,
|
|
3372
|
+
date: transcript.dateString,
|
|
3373
|
+
duration: transcript.duration ?? 0,
|
|
3374
|
+
participants: (transcript.participants ?? []).length
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
function calculatePeriod(transcripts) {
|
|
3378
|
+
let earliest = null;
|
|
3379
|
+
let latest = null;
|
|
3380
|
+
for (const t of transcripts) {
|
|
3381
|
+
const date = parseDate2(t.dateString);
|
|
3382
|
+
if (!date) continue;
|
|
3383
|
+
if (!earliest || date < earliest) {
|
|
3384
|
+
earliest = date;
|
|
3385
|
+
}
|
|
3386
|
+
if (!latest || date > latest) {
|
|
3387
|
+
latest = date;
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
return {
|
|
3391
|
+
from: earliest ? formatDateOnly2(earliest) : "",
|
|
3392
|
+
to: latest ? formatDateOnly2(latest) : ""
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
function formatDateOnly2(date) {
|
|
3396
|
+
const year = date.getUTCFullYear();
|
|
3397
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
3398
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
3399
|
+
return `${year}-${month}-${day}`;
|
|
3400
|
+
}
|
|
3401
|
+
var BUILT_IN_TEMPLATES = ["default", "compact", "executive"];
|
|
3402
|
+
function getTemplatesDir() {
|
|
3403
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
3404
|
+
return join(currentDir, "..", "templates", "digest");
|
|
3405
|
+
}
|
|
3406
|
+
function isBuiltInTemplate(name) {
|
|
3407
|
+
return BUILT_IN_TEMPLATES.includes(name);
|
|
3408
|
+
}
|
|
3409
|
+
function loadTemplate(templateOption) {
|
|
3410
|
+
const templateName = templateOption ?? "default";
|
|
3411
|
+
if (isBuiltInTemplate(templateName)) {
|
|
3412
|
+
const templatePath = join(getTemplatesDir(), `${templateName}.md`);
|
|
3413
|
+
try {
|
|
3414
|
+
return readFileSync(templatePath, "utf-8");
|
|
3415
|
+
} catch {
|
|
3416
|
+
return getInlineTemplate(templateName);
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
if (templateOption && (templateOption.startsWith("#") || templateOption.includes("{{"))) {
|
|
3420
|
+
return templateOption;
|
|
3421
|
+
}
|
|
3422
|
+
try {
|
|
3423
|
+
return readFileSync(templateName, "utf-8");
|
|
3424
|
+
} catch {
|
|
3425
|
+
if (templateName.includes("/") || templateName.includes("\\") || templateName.endsWith(".md")) {
|
|
3426
|
+
throw new Error(`Template file not found: ${templateName}`);
|
|
3427
|
+
}
|
|
3428
|
+
return templateName;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
function getInlineTemplate(name) {
|
|
3432
|
+
switch (name) {
|
|
3433
|
+
case "compact":
|
|
3434
|
+
return COMPACT_TEMPLATE;
|
|
3435
|
+
case "executive":
|
|
3436
|
+
return EXECUTIVE_TEMPLATE;
|
|
3437
|
+
default:
|
|
3438
|
+
return DEFAULT_TEMPLATE;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
function renderDigest(digest, options) {
|
|
3442
|
+
const template = loadTemplate(options?.template);
|
|
3443
|
+
return renderTemplate(template, digest);
|
|
3444
|
+
}
|
|
3445
|
+
function formatDurationHtml(minutes) {
|
|
3446
|
+
const hours = Math.floor(minutes / 60);
|
|
3447
|
+
const mins = minutes % 60;
|
|
3448
|
+
return `${hours}h ${mins}m`;
|
|
3449
|
+
}
|
|
3450
|
+
function escapeHtml(str) {
|
|
3451
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3452
|
+
}
|
|
3453
|
+
function renderActionItemsHtml(digest) {
|
|
3454
|
+
const allActionItems = digest.actionItems.byMeeting.flatMap((m) => m.items);
|
|
3455
|
+
return allActionItems.map(
|
|
3456
|
+
(item) => `
|
|
3457
|
+
<li class="action-item">
|
|
3458
|
+
<input type="checkbox" disabled />
|
|
3459
|
+
<span class="action-text">${escapeHtml(item.text)}</span>
|
|
3460
|
+
${item.assignee ? `<span class="assignee">(${escapeHtml(item.assignee)})</span>` : ""}
|
|
3461
|
+
${item.dueDate ? `<span class="due-date">due ${escapeHtml(item.dueDate)}</span>` : ""}
|
|
3462
|
+
</li>`
|
|
3463
|
+
).join("\n");
|
|
3464
|
+
}
|
|
3465
|
+
function renderMeetingsHtml(digest) {
|
|
3466
|
+
return digest.meetings.map(
|
|
3467
|
+
(m) => `
|
|
3468
|
+
<tr>
|
|
3469
|
+
<td>${escapeHtml(m.title)}</td>
|
|
3470
|
+
<td>${escapeHtml(m.date)}</td>
|
|
3471
|
+
<td>${formatDurationHtml(m.duration)}</td>
|
|
3472
|
+
<td>${m.participants}</td>
|
|
3473
|
+
</tr>`
|
|
3474
|
+
).join("\n");
|
|
3475
|
+
}
|
|
3476
|
+
function renderHighlightsHtml(digest) {
|
|
3477
|
+
return digest.highlights.map(
|
|
3478
|
+
(h) => `
|
|
3479
|
+
<div class="highlight">
|
|
3480
|
+
<h4>${escapeHtml(h.meetingTitle)} (${escapeHtml(h.meetingDate)})</h4>
|
|
3481
|
+
<ul>
|
|
3482
|
+
${h.keyPoints.map((p) => `<li>${escapeHtml(p)}</li>`).join("\n")}
|
|
3483
|
+
</ul>
|
|
3484
|
+
</div>`
|
|
3485
|
+
).join("\n");
|
|
3486
|
+
}
|
|
3487
|
+
function renderParticipantsHtml(digest) {
|
|
3488
|
+
return digest.participants.map(
|
|
3489
|
+
(p) => `
|
|
3490
|
+
<tr>
|
|
3491
|
+
<td>${escapeHtml(p.name || p.email)}</td>
|
|
3492
|
+
<td>${escapeHtml(p.email)}</td>
|
|
3493
|
+
<td>${p.meetingCount}</td>
|
|
3494
|
+
<td>${formatDurationHtml(p.totalMinutes)}</td>
|
|
3495
|
+
</tr>`
|
|
3496
|
+
).join("\n");
|
|
3497
|
+
}
|
|
3498
|
+
function renderDigestHtml(digest) {
|
|
3499
|
+
const actionItemsHtml = renderActionItemsHtml(digest);
|
|
3500
|
+
const meetingsHtml = renderMeetingsHtml(digest);
|
|
3501
|
+
const highlightsHtml = renderHighlightsHtml(digest);
|
|
3502
|
+
const participantsHtml = renderParticipantsHtml(digest);
|
|
3503
|
+
return `<!DOCTYPE html>
|
|
3504
|
+
<html lang="en">
|
|
3505
|
+
<head>
|
|
3506
|
+
<meta charset="UTF-8">
|
|
3507
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3508
|
+
<title>Weekly Meeting Digest</title>
|
|
3509
|
+
<style>
|
|
3510
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; line-height: 1.6; }
|
|
3511
|
+
h1 { color: #333; border-bottom: 2px solid #4a90d9; padding-bottom: 10px; }
|
|
3512
|
+
h2 { color: #4a90d9; margin-top: 30px; }
|
|
3513
|
+
h3 { color: #666; }
|
|
3514
|
+
.overview { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0; }
|
|
3515
|
+
.stat-card { background: #f5f5f5; padding: 20px; border-radius: 8px; text-align: center; }
|
|
3516
|
+
.stat-value { font-size: 2em; font-weight: bold; color: #4a90d9; }
|
|
3517
|
+
.stat-label { color: #666; }
|
|
3518
|
+
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
|
3519
|
+
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
|
|
3520
|
+
th { background: #f5f5f5; font-weight: 600; }
|
|
3521
|
+
.action-item { list-style: none; padding: 8px 0; border-bottom: 1px solid #eee; }
|
|
3522
|
+
.action-item input { margin-right: 10px; }
|
|
3523
|
+
.assignee { color: #4a90d9; margin-left: 10px; }
|
|
3524
|
+
.due-date { color: #e74c3c; margin-left: 10px; font-size: 0.9em; }
|
|
3525
|
+
.highlight { background: #f9f9f9; padding: 15px; border-radius: 8px; margin: 10px 0; }
|
|
3526
|
+
.highlight h4 { margin: 0 0 10px 0; color: #333; }
|
|
3527
|
+
.highlight ul { margin: 0; padding-left: 20px; }
|
|
3528
|
+
footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; color: #999; font-size: 0.9em; }
|
|
3529
|
+
</style>
|
|
3530
|
+
</head>
|
|
3531
|
+
<body>
|
|
3532
|
+
<h1>Weekly Meeting Digest</h1>
|
|
3533
|
+
<p><strong>${escapeHtml(digest.period.from)}</strong> to <strong>${escapeHtml(digest.period.to)}</strong></p>
|
|
3534
|
+
|
|
3535
|
+
<div class="overview">
|
|
3536
|
+
<div class="stat-card">
|
|
3537
|
+
<div class="stat-value">${digest.totalMeetings}</div>
|
|
3538
|
+
<div class="stat-label">Meetings</div>
|
|
3539
|
+
</div>
|
|
3540
|
+
<div class="stat-card">
|
|
3541
|
+
<div class="stat-value">${formatDurationHtml(digest.totalDuration)}</div>
|
|
3542
|
+
<div class="stat-label">Total Time</div>
|
|
3543
|
+
</div>
|
|
3544
|
+
<div class="stat-card">
|
|
3545
|
+
<div class="stat-value">${digest.actionItems.total}</div>
|
|
3546
|
+
<div class="stat-label">Action Items</div>
|
|
3547
|
+
</div>
|
|
3548
|
+
</div>
|
|
3549
|
+
|
|
3550
|
+
<h2>Meeting Stats</h2>
|
|
3551
|
+
<p>Busiest day: <strong>${escapeHtml(digest.stats.busiestDay)}</strong></p>
|
|
3552
|
+
<p>Average duration: <strong>${formatDurationHtml(digest.stats.averageDuration)}</strong></p>
|
|
3553
|
+
|
|
3554
|
+
<h2>Meetings</h2>
|
|
3555
|
+
<table>
|
|
3556
|
+
<thead>
|
|
3557
|
+
<tr><th>Title</th><th>Date</th><th>Duration</th><th>Participants</th></tr>
|
|
3558
|
+
</thead>
|
|
3559
|
+
<tbody>
|
|
3560
|
+
${meetingsHtml}
|
|
3561
|
+
</tbody>
|
|
3562
|
+
</table>
|
|
3563
|
+
|
|
3564
|
+
${digest.actionItems.total > 0 ? `
|
|
3565
|
+
<h2>Action Items (${digest.actionItems.total})</h2>
|
|
3566
|
+
<ul style="padding-left: 0;">
|
|
3567
|
+
${actionItemsHtml}
|
|
3568
|
+
</ul>` : ""}
|
|
3569
|
+
|
|
3570
|
+
${digest.highlights.length > 0 ? `
|
|
3571
|
+
<h2>Highlights</h2>
|
|
3572
|
+
${highlightsHtml}` : ""}
|
|
3573
|
+
|
|
3574
|
+
<h2>Participants</h2>
|
|
3575
|
+
<table>
|
|
3576
|
+
<thead>
|
|
3577
|
+
<tr><th>Name</th><th>Email</th><th>Meetings</th><th>Time</th></tr>
|
|
3578
|
+
</thead>
|
|
3579
|
+
<tbody>
|
|
3580
|
+
${participantsHtml}
|
|
3581
|
+
</tbody>
|
|
3582
|
+
</table>
|
|
3583
|
+
|
|
3584
|
+
<footer>
|
|
3585
|
+
Generated with fireflies-api
|
|
3586
|
+
</footer>
|
|
3587
|
+
</body>
|
|
3588
|
+
</html>`;
|
|
3589
|
+
}
|
|
3590
|
+
var FILTERS = {
|
|
3591
|
+
duration: (value) => {
|
|
3592
|
+
const minutes = Math.round(Number(value) || 0);
|
|
3593
|
+
const hours = Math.floor(minutes / 60);
|
|
3594
|
+
const mins = minutes % 60;
|
|
3595
|
+
return `${hours}h ${mins}m`;
|
|
3596
|
+
},
|
|
3597
|
+
date: (value) => {
|
|
3598
|
+
const str = String(value || "");
|
|
3599
|
+
if (!str) return "";
|
|
3600
|
+
const date = new Date(str);
|
|
3601
|
+
if (Number.isNaN(date.getTime())) return str;
|
|
3602
|
+
return date.toLocaleDateString("en-US", {
|
|
3603
|
+
month: "short",
|
|
3604
|
+
day: "numeric",
|
|
3605
|
+
year: "numeric"
|
|
3606
|
+
});
|
|
3607
|
+
},
|
|
3608
|
+
join: (value) => {
|
|
3609
|
+
if (Array.isArray(value)) {
|
|
3610
|
+
return value.join(", ");
|
|
3611
|
+
}
|
|
3612
|
+
return String(value || "");
|
|
3613
|
+
},
|
|
3614
|
+
lowercase: (value) => String(value || "").toLowerCase(),
|
|
3615
|
+
uppercase: (value) => String(value || "").toUpperCase()
|
|
3616
|
+
};
|
|
3617
|
+
function renderTemplate(template, data) {
|
|
3618
|
+
if (!template) return "";
|
|
3619
|
+
let result = template;
|
|
3620
|
+
result = processSections(result, data);
|
|
3621
|
+
result = processVariables(result, data);
|
|
3622
|
+
return result;
|
|
3623
|
+
}
|
|
3624
|
+
function processSections(template, data) {
|
|
3625
|
+
const sectionPattern = /\{\{#(\w+(?:\.\w+)*)\}\}([\s\S]*?)\{\{\/\1\}\}/g;
|
|
3626
|
+
let result = template;
|
|
3627
|
+
let iterations = 0;
|
|
3628
|
+
const maxIterations = 100;
|
|
3629
|
+
for (; ; ) {
|
|
3630
|
+
const match = sectionPattern.exec(result);
|
|
3631
|
+
if (!match || iterations >= maxIterations) break;
|
|
3632
|
+
iterations++;
|
|
3633
|
+
const [fullMatch, path, content] = match;
|
|
3634
|
+
if (!path || content === void 0) continue;
|
|
3635
|
+
const value = getNestedValue(data, path);
|
|
3636
|
+
const replacement = renderSectionValue(value, content, data);
|
|
3637
|
+
result = result.slice(0, match.index) + replacement + result.slice(match.index + fullMatch.length);
|
|
3638
|
+
sectionPattern.lastIndex = 0;
|
|
3639
|
+
}
|
|
3640
|
+
return result;
|
|
3641
|
+
}
|
|
3642
|
+
function renderSectionValue(value, content, data) {
|
|
3643
|
+
if (Array.isArray(value)) {
|
|
3644
|
+
return renderArraySection(value, content, data);
|
|
3645
|
+
}
|
|
3646
|
+
if (isIterableObject(value)) {
|
|
3647
|
+
return renderObjectSection(value, content, data);
|
|
3648
|
+
}
|
|
3649
|
+
if (isTruthy(value)) {
|
|
3650
|
+
const processed = processSections(content, data);
|
|
3651
|
+
return processVariables(processed, data);
|
|
3652
|
+
}
|
|
3653
|
+
return "";
|
|
3654
|
+
}
|
|
3655
|
+
function renderArraySection(items, content, data) {
|
|
3656
|
+
let result = "";
|
|
3657
|
+
for (const item of items) {
|
|
3658
|
+
if (typeof item === "object" && item !== null) {
|
|
3659
|
+
const itemData = { ...data, ...item };
|
|
3660
|
+
const processed = processSections(content, itemData);
|
|
3661
|
+
result += processVariables(processed, itemData);
|
|
3662
|
+
} else {
|
|
3663
|
+
const itemContent = content.replace(/\{\{\.\}\}/g, String(item));
|
|
3664
|
+
result += processVariables(itemContent, data);
|
|
2610
3665
|
}
|
|
2611
3666
|
}
|
|
3667
|
+
return result;
|
|
2612
3668
|
}
|
|
2613
|
-
function
|
|
2614
|
-
|
|
3669
|
+
function renderObjectSection(obj, content, data) {
|
|
3670
|
+
let result = "";
|
|
3671
|
+
for (const [key, itemValue] of Object.entries(obj)) {
|
|
3672
|
+
const itemData = buildObjectItemData(data, key, itemValue);
|
|
3673
|
+
const itemContent = content.replace(/\{\{\.\}\}/g, key);
|
|
3674
|
+
const processed = processSections(itemContent, itemData);
|
|
3675
|
+
result += processVariables(processed, itemData);
|
|
3676
|
+
}
|
|
3677
|
+
return result;
|
|
3678
|
+
}
|
|
3679
|
+
function buildObjectItemData(data, key, itemValue) {
|
|
3680
|
+
const baseData = { ...data, _key: key, _value: itemValue };
|
|
3681
|
+
if (typeof itemValue === "object" && itemValue !== null && !Array.isArray(itemValue)) {
|
|
3682
|
+
return { ...baseData, ...itemValue };
|
|
3683
|
+
}
|
|
3684
|
+
return baseData;
|
|
3685
|
+
}
|
|
3686
|
+
function isIterableObject(value) {
|
|
3687
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
3688
|
+
}
|
|
3689
|
+
function isTruthy(value) {
|
|
3690
|
+
if (value === null || value === void 0) return false;
|
|
3691
|
+
if (typeof value === "boolean") return value;
|
|
3692
|
+
if (typeof value === "string") return value.length > 0;
|
|
3693
|
+
if (typeof value === "number") return true;
|
|
3694
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
3695
|
+
return true;
|
|
3696
|
+
}
|
|
3697
|
+
function processVariables(template, data) {
|
|
3698
|
+
return template.replace(
|
|
3699
|
+
/\{\{([\w.]+)(?:\s*\|\s*(\w+))?\}\}/g,
|
|
3700
|
+
(_match, path, filterName) => {
|
|
3701
|
+
const value = getNestedValue(data, path);
|
|
3702
|
+
if (value === void 0 || value === null) {
|
|
3703
|
+
return "";
|
|
3704
|
+
}
|
|
3705
|
+
if (filterName && FILTERS[filterName]) {
|
|
3706
|
+
return FILTERS[filterName](value);
|
|
3707
|
+
}
|
|
3708
|
+
return String(value);
|
|
3709
|
+
}
|
|
3710
|
+
);
|
|
3711
|
+
}
|
|
3712
|
+
function getNestedValue(obj, path) {
|
|
3713
|
+
const parts = path.split(".");
|
|
3714
|
+
let current = obj;
|
|
3715
|
+
for (const part of parts) {
|
|
3716
|
+
if (current === null || current === void 0) {
|
|
3717
|
+
return void 0;
|
|
3718
|
+
}
|
|
3719
|
+
if (typeof current !== "object") {
|
|
3720
|
+
return void 0;
|
|
3721
|
+
}
|
|
3722
|
+
current = current[part];
|
|
3723
|
+
}
|
|
3724
|
+
return current;
|
|
2615
3725
|
}
|
|
3726
|
+
var DEFAULT_TEMPLATE = `# Weekly Meeting Digest
|
|
3727
|
+
**{{period.from}} to {{period.to}}**
|
|
3728
|
+
|
|
3729
|
+
## Overview
|
|
3730
|
+
- **{{totalMeetings}}** meetings
|
|
3731
|
+
- **{{totalDuration | duration}}** total time
|
|
3732
|
+
- **{{actionItems.total}}** action items
|
|
3733
|
+
|
|
3734
|
+
## Meeting Stats
|
|
3735
|
+
{{#stats.meetingsByDay}}
|
|
3736
|
+
- {{.}}
|
|
3737
|
+
{{/stats.meetingsByDay}}
|
|
3738
|
+
|
|
3739
|
+
Busiest day: {{stats.busiestDay}}
|
|
3740
|
+
Average duration: {{stats.averageDuration}} minutes
|
|
3741
|
+
|
|
3742
|
+
## Action Items
|
|
3743
|
+
{{#actionItems.byAssignee}}
|
|
3744
|
+
### {{.}}
|
|
3745
|
+
{{/actionItems.byAssignee}}
|
|
3746
|
+
|
|
3747
|
+
{{#actionItems.unassigned}}
|
|
3748
|
+
### Unassigned
|
|
3749
|
+
- [ ] {{text}}
|
|
3750
|
+
{{/actionItems.unassigned}}
|
|
3751
|
+
|
|
3752
|
+
## Highlights
|
|
3753
|
+
{{#highlights}}
|
|
3754
|
+
### {{meetingTitle}} ({{meetingDate | date}})
|
|
3755
|
+
{{#keyPoints}}
|
|
3756
|
+
- {{.}}
|
|
3757
|
+
{{/keyPoints}}
|
|
3758
|
+
{{/highlights}}
|
|
3759
|
+
|
|
3760
|
+
## Participants
|
|
3761
|
+
{{#participants}}
|
|
3762
|
+
- {{email}} ({{meetingCount}} meetings, {{totalMinutes | duration}})
|
|
3763
|
+
{{/participants}}
|
|
3764
|
+
|
|
3765
|
+
---
|
|
3766
|
+
*Generated with fireflies-api*
|
|
3767
|
+
`;
|
|
3768
|
+
var COMPACT_TEMPLATE = `# Weekly Digest: {{period.from}} - {{period.to}}
|
|
3769
|
+
|
|
3770
|
+
**{{totalMeetings}}** meetings | **{{totalDuration | duration}}** | **{{actionItems.total}}** action items
|
|
3771
|
+
|
|
3772
|
+
{{#actionItems.unassigned}}
|
|
3773
|
+
## Action Items
|
|
3774
|
+
- [ ] {{text}}
|
|
3775
|
+
{{/actionItems.unassigned}}
|
|
3776
|
+
`;
|
|
3777
|
+
var EXECUTIVE_TEMPLATE = `# Executive Summary
|
|
3778
|
+
**Weekly Meeting Report: {{period.from}} to {{period.to}}**
|
|
3779
|
+
|
|
3780
|
+
## Key Metrics
|
|
3781
|
+
| Metric | Value |
|
|
3782
|
+
|--------|-------|
|
|
3783
|
+
| Total Meetings | {{totalMeetings}} |
|
|
3784
|
+
| Total Time | {{totalDuration | duration}} |
|
|
3785
|
+
| Action Items | {{actionItems.total}} |
|
|
3786
|
+
| Participants | {{participants.length}} |
|
|
3787
|
+
|
|
3788
|
+
## Top Highlights
|
|
3789
|
+
{{#highlights}}
|
|
3790
|
+
- **{{meetingTitle}}**: {{#keyPoints}}{{.}} {{/keyPoints}}
|
|
3791
|
+
{{/highlights}}
|
|
3792
|
+
|
|
3793
|
+
## Outstanding Action Items
|
|
3794
|
+
{{#actionItems.unassigned}}
|
|
3795
|
+
- {{text}}
|
|
3796
|
+
{{/actionItems.unassigned}}
|
|
3797
|
+
|
|
3798
|
+
---
|
|
3799
|
+
*Executive Summary generated with fireflies-api*
|
|
3800
|
+
`;
|
|
2616
3801
|
|
|
2617
3802
|
// src/helpers/external-questions.ts
|
|
2618
3803
|
function findExternalParticipantQuestions(transcript, internalDomains) {
|
|
@@ -2687,227 +3872,6 @@ function isInternal(email, internalDomains) {
|
|
|
2687
3872
|
return internalDomains.some((domain) => lowerEmail.endsWith(domain));
|
|
2688
3873
|
}
|
|
2689
3874
|
|
|
2690
|
-
// src/helpers/markdown.ts
|
|
2691
|
-
var DEFAULT_OPTIONS2 = {
|
|
2692
|
-
includeMetadata: true,
|
|
2693
|
-
includeSummary: true,
|
|
2694
|
-
includeActionItems: true,
|
|
2695
|
-
actionItemFormat: "checkbox",
|
|
2696
|
-
includeTimestamps: false,
|
|
2697
|
-
speakerFormat: "bold",
|
|
2698
|
-
groupBySpeaker: true
|
|
2699
|
-
};
|
|
2700
|
-
var DEFAULT_CHUNKS_OPTIONS = {
|
|
2701
|
-
title: "Live Transcript",
|
|
2702
|
-
includeTimestamps: false,
|
|
2703
|
-
speakerFormat: "bold",
|
|
2704
|
-
groupBySpeaker: true
|
|
2705
|
-
};
|
|
2706
|
-
async function transcriptToMarkdown(transcript, options = {}) {
|
|
2707
|
-
const opts = { ...DEFAULT_OPTIONS2, ...options };
|
|
2708
|
-
const sections = [];
|
|
2709
|
-
if (opts.includeMetadata) {
|
|
2710
|
-
sections.push(formatMetadata(transcript));
|
|
2711
|
-
}
|
|
2712
|
-
if (opts.includeSummary && transcript.summary) {
|
|
2713
|
-
sections.push(formatSummary(transcript.summary, opts));
|
|
2714
|
-
}
|
|
2715
|
-
if (transcript.sentences && transcript.sentences.length > 0) {
|
|
2716
|
-
sections.push(formatTranscript(transcript.sentences, opts));
|
|
2717
|
-
}
|
|
2718
|
-
const content = sections.join("\n\n---\n\n");
|
|
2719
|
-
await writeIfOutputPath(content, options.outputPath);
|
|
2720
|
-
return content;
|
|
2721
|
-
}
|
|
2722
|
-
async function chunksToMarkdown(chunks, options = {}) {
|
|
2723
|
-
const opts = { ...DEFAULT_CHUNKS_OPTIONS, ...options };
|
|
2724
|
-
const lines = [`# ${opts.title}`];
|
|
2725
|
-
if (chunks.length === 0) {
|
|
2726
|
-
lines.push("", "## Transcript", "", "*No transcription data*");
|
|
2727
|
-
} else {
|
|
2728
|
-
lines.push("", "## Transcript");
|
|
2729
|
-
if (opts.groupBySpeaker) {
|
|
2730
|
-
const groups = groupChunksBySpeaker(chunks);
|
|
2731
|
-
for (const group of groups) {
|
|
2732
|
-
lines.push("", formatChunkGroup(group, opts));
|
|
2733
|
-
}
|
|
2734
|
-
} else {
|
|
2735
|
-
for (const chunk of chunks) {
|
|
2736
|
-
lines.push("", formatChunk(chunk, opts));
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2739
|
-
}
|
|
2740
|
-
const content = lines.join("\n");
|
|
2741
|
-
await writeIfOutputPath(content, options.outputPath);
|
|
2742
|
-
return content;
|
|
2743
|
-
}
|
|
2744
|
-
function formatMetadata(transcript) {
|
|
2745
|
-
const lines = [`# ${transcript.title || "Untitled Meeting"}`];
|
|
2746
|
-
if (transcript.dateString) {
|
|
2747
|
-
lines.push(`
|
|
2748
|
-
**Date:** ${formatDate(transcript.dateString)}`);
|
|
2749
|
-
}
|
|
2750
|
-
const duration = calculateDuration(transcript);
|
|
2751
|
-
if (duration > 0) {
|
|
2752
|
-
lines.push(`**Duration:** ${formatDuration(duration)}`);
|
|
2753
|
-
}
|
|
2754
|
-
const participants = getParticipantNames(transcript);
|
|
2755
|
-
if (participants.length > 0) {
|
|
2756
|
-
lines.push(`**Participants:** ${participants.join(", ")}`);
|
|
2757
|
-
}
|
|
2758
|
-
return lines.join("\n");
|
|
2759
|
-
}
|
|
2760
|
-
function calculateDuration(transcript) {
|
|
2761
|
-
if (transcript.sentences && transcript.sentences.length > 0) {
|
|
2762
|
-
const lastSentence = transcript.sentences[transcript.sentences.length - 1];
|
|
2763
|
-
if (lastSentence) {
|
|
2764
|
-
return parseFloat(lastSentence.end_time);
|
|
2765
|
-
}
|
|
2766
|
-
}
|
|
2767
|
-
return transcript.duration || 0;
|
|
2768
|
-
}
|
|
2769
|
-
function formatSummary(summary, opts) {
|
|
2770
|
-
const sections = ["## Summary"];
|
|
2771
|
-
if (summary.gist) {
|
|
2772
|
-
sections.push("", summary.gist);
|
|
2773
|
-
}
|
|
2774
|
-
if (summary.bullet_gist) {
|
|
2775
|
-
const bullets = parseMultilineField(summary.bullet_gist);
|
|
2776
|
-
if (bullets.length > 0) {
|
|
2777
|
-
sections.push("", "### Key Points");
|
|
2778
|
-
sections.push(bullets.map((p) => `- ${p}`).join("\n"));
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
if (opts.includeActionItems && summary.action_items) {
|
|
2782
|
-
const items = parseMultilineField(summary.action_items);
|
|
2783
|
-
if (items.length > 0) {
|
|
2784
|
-
sections.push("", "### Action Items");
|
|
2785
|
-
const prefix = opts.actionItemFormat === "checkbox" ? "- [ ] " : "- ";
|
|
2786
|
-
sections.push(items.map((a) => `${prefix}${a}`).join("\n"));
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
return sections.join("\n");
|
|
2790
|
-
}
|
|
2791
|
-
function formatTranscript(sentences, opts) {
|
|
2792
|
-
const lines = ["## Transcript"];
|
|
2793
|
-
if (opts.groupBySpeaker) {
|
|
2794
|
-
const groups = groupSentencesBySpeaker(sentences);
|
|
2795
|
-
for (const group of groups) {
|
|
2796
|
-
lines.push("", formatSpeakerGroup(group, opts));
|
|
2797
|
-
}
|
|
2798
|
-
} else {
|
|
2799
|
-
for (const sentence of sentences) {
|
|
2800
|
-
lines.push("", formatSentence(sentence, opts));
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
return lines.join("\n");
|
|
2804
|
-
}
|
|
2805
|
-
function groupSentencesBySpeaker(sentences) {
|
|
2806
|
-
const groups = [];
|
|
2807
|
-
let current = null;
|
|
2808
|
-
for (const sentence of sentences) {
|
|
2809
|
-
if (!current || current.speakerName !== sentence.speaker_name) {
|
|
2810
|
-
current = { speakerName: sentence.speaker_name, sentences: [] };
|
|
2811
|
-
groups.push(current);
|
|
2812
|
-
}
|
|
2813
|
-
current.sentences.push(sentence);
|
|
2814
|
-
}
|
|
2815
|
-
return groups;
|
|
2816
|
-
}
|
|
2817
|
-
function formatSpeakerGroup(group, opts) {
|
|
2818
|
-
const speaker = formatSpeakerName(group.speakerName, opts.speakerFormat);
|
|
2819
|
-
const text = group.sentences.map((s) => s.text).join(" ");
|
|
2820
|
-
const firstSentence = group.sentences[0];
|
|
2821
|
-
if (opts.includeTimestamps && firstSentence) {
|
|
2822
|
-
const timestamp = formatTimestamp(firstSentence.start_time);
|
|
2823
|
-
return `${timestamp} ${speaker} ${text}`;
|
|
2824
|
-
}
|
|
2825
|
-
return `${speaker} ${text}`;
|
|
2826
|
-
}
|
|
2827
|
-
function formatSentence(sentence, opts) {
|
|
2828
|
-
const speaker = formatSpeakerName(sentence.speaker_name, opts.speakerFormat);
|
|
2829
|
-
if (opts.includeTimestamps) {
|
|
2830
|
-
const timestamp = formatTimestamp(sentence.start_time);
|
|
2831
|
-
return `${timestamp} ${speaker} ${sentence.text}`;
|
|
2832
|
-
}
|
|
2833
|
-
return `${speaker} ${sentence.text}`;
|
|
2834
|
-
}
|
|
2835
|
-
function groupChunksBySpeaker(chunks) {
|
|
2836
|
-
const groups = [];
|
|
2837
|
-
let current = null;
|
|
2838
|
-
for (const chunk of chunks) {
|
|
2839
|
-
if (!current || current.speakerName !== chunk.speaker_name) {
|
|
2840
|
-
current = { speakerName: chunk.speaker_name, chunks: [] };
|
|
2841
|
-
groups.push(current);
|
|
2842
|
-
}
|
|
2843
|
-
current.chunks.push(chunk);
|
|
2844
|
-
}
|
|
2845
|
-
return groups;
|
|
2846
|
-
}
|
|
2847
|
-
function formatChunkGroup(group, opts) {
|
|
2848
|
-
const speaker = formatSpeakerName(group.speakerName, opts.speakerFormat);
|
|
2849
|
-
const text = group.chunks.map((c) => c.text).join(" ");
|
|
2850
|
-
const firstChunk = group.chunks[0];
|
|
2851
|
-
if (opts.includeTimestamps && firstChunk) {
|
|
2852
|
-
const timestamp = formatTimestamp(firstChunk.start_time.toString());
|
|
2853
|
-
return `${timestamp} ${speaker} ${text}`;
|
|
2854
|
-
}
|
|
2855
|
-
return `${speaker} ${text}`;
|
|
2856
|
-
}
|
|
2857
|
-
function formatChunk(chunk, opts) {
|
|
2858
|
-
const speaker = formatSpeakerName(chunk.speaker_name, opts.speakerFormat);
|
|
2859
|
-
if (opts.includeTimestamps) {
|
|
2860
|
-
const timestamp = formatTimestamp(chunk.start_time.toString());
|
|
2861
|
-
return `${timestamp} ${speaker} ${chunk.text}`;
|
|
2862
|
-
}
|
|
2863
|
-
return `${speaker} ${chunk.text}`;
|
|
2864
|
-
}
|
|
2865
|
-
function formatSpeakerName(name, format) {
|
|
2866
|
-
switch (format) {
|
|
2867
|
-
case "bold":
|
|
2868
|
-
return `**${name}:**`;
|
|
2869
|
-
case "plain":
|
|
2870
|
-
return `${name}:`;
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
function formatTimestamp(startTime) {
|
|
2874
|
-
const seconds = parseFloat(startTime);
|
|
2875
|
-
const mins = Math.floor(seconds / 60);
|
|
2876
|
-
const secs = Math.floor(seconds % 60);
|
|
2877
|
-
return `[${mins}:${secs.toString().padStart(2, "0")}]`;
|
|
2878
|
-
}
|
|
2879
|
-
function formatDuration(seconds) {
|
|
2880
|
-
const hours = Math.floor(seconds / 3600);
|
|
2881
|
-
const mins = Math.floor(seconds % 3600 / 60);
|
|
2882
|
-
if (hours > 0) {
|
|
2883
|
-
return `${hours}h ${mins}m`;
|
|
2884
|
-
}
|
|
2885
|
-
return `${mins} minutes`;
|
|
2886
|
-
}
|
|
2887
|
-
function formatDate(isoString) {
|
|
2888
|
-
return new Date(isoString).toLocaleDateString("en-US", {
|
|
2889
|
-
weekday: "long",
|
|
2890
|
-
year: "numeric",
|
|
2891
|
-
month: "long",
|
|
2892
|
-
day: "numeric"
|
|
2893
|
-
});
|
|
2894
|
-
}
|
|
2895
|
-
function getParticipantNames(transcript) {
|
|
2896
|
-
if (transcript.meeting_attendees?.length) {
|
|
2897
|
-
return transcript.meeting_attendees.map((a) => a.displayName || a.name || a.email).filter(Boolean);
|
|
2898
|
-
}
|
|
2899
|
-
return transcript.speakers?.map((s) => s.name) || [];
|
|
2900
|
-
}
|
|
2901
|
-
function parseMultilineField(value) {
|
|
2902
|
-
return value.split(/\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2903
|
-
}
|
|
2904
|
-
async function writeIfOutputPath(content, outputPath) {
|
|
2905
|
-
if (outputPath) {
|
|
2906
|
-
const { writeFile } = await import('fs/promises');
|
|
2907
|
-
await writeFile(outputPath, content, "utf-8");
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
|
|
2911
3875
|
// src/utils/dedup.ts
|
|
2912
3876
|
var Deduplicator = class {
|
|
2913
3877
|
seen = /* @__PURE__ */ new Set();
|
|
@@ -3339,6 +4303,6 @@ function parseWebhookPayload(payload, options) {
|
|
|
3339
4303
|
return payload;
|
|
3340
4304
|
}
|
|
3341
4305
|
|
|
3342
|
-
export { AuthenticationError, ChunkTimeoutError, ConnectionError, Deduplicator, FirefliesClient, FirefliesError, GraphQLError, NetworkError, NotFoundError, RateLimitError, RealtimeError, RealtimeStream, StreamClosedError, TimeoutError, TranscriptAccumulator, ValidationError, WebhookParseError, WebhookVerificationError, aggregateActionItems, analyzeMeetings, analyzeSpeakers, batch, batchAll, chunksToMarkdown, collectAll, createNormalizer, extractActionItems, extractDomain, filterActionItems, findExternalParticipantQuestions, formatActionItemsMarkdown, getMeetingVideos, getMeetingsForMultipleUsers, hasExternalParticipants, hasVideo, isValidWebhookPayload, normalizeTranscript2 as normalizeTranscript, normalizeTranscripts, normalizeTranscriptsAll, paginate, parseWebhookPayload, searchTranscript, transcriptToMarkdown, verifyWebhookSignature };
|
|
4306
|
+
export { AuthenticationError, ChunkTimeoutError, ConnectionError, Deduplicator, FirefliesClient, FirefliesError, GraphQLError, NetworkError, NotFoundError, RateLimitError, RealtimeError, RealtimeStream, StreamClosedError, TimeoutError, TranscriptAccumulator, ValidationError, WebhookParseError, WebhookVerificationError, aggregateActionItems, aggregateActionItemsForDigest, aggregateParticipants2 as aggregateParticipants, analyzeMeetings, analyzeSpeakers, batch, batchAll, buildDigest, calculateStats, chunksToMarkdown, collectAll, createNormalizer, createZipArchive, exportTranscript, extractActionItems, extractDomain, extractHighlights, filterActionItems, findExternalParticipantQuestions, formatActionItemsMarkdown, generateExportFilename, getMeetingVideos, getMeetingsForMultipleUsers, hasExternalParticipants, hasVideo, isValidWebhookPayload, normalizeTranscript2 as normalizeTranscript, normalizeTranscripts, normalizeTranscriptsAll, paginate, paginateParallel, parseWebhookPayload, renderDigest, renderDigestHtml, renderTemplate, sanitizeFilename, searchTranscript, transcriptToCsv, transcriptToMarkdown, transcriptToText, verifyWebhookSignature };
|
|
3343
4307
|
//# sourceMappingURL=index.js.map
|
|
3344
4308
|
//# sourceMappingURL=index.js.map
|