Skip to content

Commit be3acc0

Browse files
committed
Add autogenerated meeting notes to recordings
- Add NOTES_PROMPT for structured meeting notes generation - Generate notes during transcription using GPT-4o-mini - Display notes section between Extracted Tasks and Transcript - Notes include: Overview, Key Points, Action Items, Details - Custom styling for readable, scannable note formatting
1 parent 57172c5 commit be3acc0

File tree

4 files changed

+141
-17
lines changed

4 files changed

+141
-17
lines changed

src/main/services/recording.ts

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,40 @@ import { z } from "zod";
88
import type { Recording } from "../../shared/types.js";
99

1010
import {
11+
NOTES_PROMPT,
1112
SUMMARY_PROMPT,
1213
TASK_EXTRACTION_PROMPT,
1314
} from "./transcription-prompts.js";
1415

15-
let FileConstructor: typeof File;
16-
try {
17-
const { File: NodeFile } = await import("node:buffer");
18-
FileConstructor = NodeFile as typeof File;
19-
} catch {
20-
FileConstructor = class File extends Blob {
21-
name: string;
22-
lastModified: number;
23-
24-
constructor(bits: BlobPart[], name: string, options?: FilePropertyBag) {
25-
super(bits, options);
26-
this.name = name;
27-
this.lastModified = options?.lastModified ?? Date.now();
28-
}
29-
} as typeof File;
30-
}
16+
// Lazy initialization to avoid top-level await
17+
let fileConstructorInitialized = false;
18+
19+
function initializeFileConstructor(): void {
20+
if (fileConstructorInitialized || globalThis.File) {
21+
return;
22+
}
23+
24+
let FileConstructor: typeof File;
25+
try {
26+
// Use synchronous require for node:buffer to avoid top-level await
27+
// eslint-disable-next-line @typescript-eslint/no-var-requires
28+
const { File: NodeFile } = require("node:buffer");
29+
FileConstructor = NodeFile as typeof File;
30+
} catch {
31+
FileConstructor = class File extends Blob {
32+
name: string;
33+
lastModified: number;
34+
35+
constructor(bits: BlobPart[], name: string, options?: FilePropertyBag) {
36+
super(bits, options);
37+
this.name = name;
38+
this.lastModified = options?.lastModified ?? Date.now();
39+
}
40+
} as typeof File;
41+
}
3142

32-
if (!globalThis.File) {
3343
globalThis.File = FileConstructor;
44+
fileConstructorInitialized = true;
3445
}
3546

3647
interface RecordingSession {
@@ -130,6 +141,40 @@ async function extractTasksFromTranscript(
130141
}
131142
}
132143

144+
async function generateNotesFromTranscript(
145+
transcriptText: string,
146+
openaiApiKey: string,
147+
): Promise<string | null> {
148+
try {
149+
const openai = createOpenAI({ apiKey: openaiApiKey });
150+
151+
const { object } = await generateObject({
152+
model: openai("gpt-4o-mini"),
153+
schema: z.object({
154+
notes: z
155+
.string()
156+
.describe("Structured meeting notes in markdown format"),
157+
}),
158+
messages: [
159+
{
160+
role: "system",
161+
content:
162+
"You are a meeting notes generator that creates structured, scannable notes from transcripts. Follow the format exactly and be terse.",
163+
},
164+
{
165+
role: "user",
166+
content: `${NOTES_PROMPT}\n${transcriptText}`,
167+
},
168+
],
169+
});
170+
171+
return object.notes || null;
172+
} catch (error) {
173+
console.error("Failed to generate notes from transcription:", error);
174+
return null;
175+
}
176+
}
177+
133178
function safeLog(...args: unknown[]): void {
134179
try {
135180
console.log(...args);
@@ -139,6 +184,9 @@ function safeLog(...args: unknown[]): void {
139184
}
140185

141186
export function registerRecordingIpc(): void {
187+
// Initialize File constructor polyfill
188+
initializeFileConstructor();
189+
142190
ipcMain.handle(
143191
"desktop-capturer:get-sources",
144192
async (_event, options: { types: ("screen" | "window")[] }) => {
@@ -369,6 +417,11 @@ export function registerRecordingIpc(): void {
369417
openaiApiKey,
370418
);
371419

420+
const notesContent = await generateNotesFromTranscript(
421+
fullTranscriptText,
422+
openaiApiKey,
423+
);
424+
372425
const extractedTasks = await extractTasksFromTranscript(
373426
fullTranscriptText,
374427
openaiApiKey,
@@ -382,6 +435,7 @@ export function registerRecordingIpc(): void {
382435
status: "completed",
383436
text: fullTranscriptText,
384437
summary: summaryTitle,
438+
notes: notesContent,
385439
extracted_tasks: extractedTasks,
386440
};
387441
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
@@ -390,6 +444,7 @@ export function registerRecordingIpc(): void {
390444
status: "completed",
391445
text: fullTranscriptText,
392446
summary: summaryTitle,
447+
notes: notesContent,
393448
extracted_tasks: extractedTasks,
394449
};
395450
} catch (error) {

src/main/services/transcription-prompts.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,31 @@ For each task, provide a clear title and a description with relevant context fro
2323
If there are no actionable tasks, return an empty tasks array.
2424
2525
Transcript:`;
26+
27+
export const NOTES_PROMPT = `You are a meeting notes generator. You receive raw transcripts and produce structured, scannable notes.
28+
29+
Generate notes in the following markdown format:
30+
31+
## Overview
32+
[2-3 sentence summary: what was this meeting about, what got decided/discussed]
33+
34+
## Key Points
35+
- [main discussion topics, decisions, or conclusions]
36+
- [include specific details: numbers, names, commitments]
37+
- [capture disagreements or open questions]
38+
39+
## Action Items
40+
- [who needs to do what, if clear from context]
41+
- [outstanding questions or follow-ups needed]
42+
43+
## Details
44+
[anything substantive that doesn't fit above - technical specifics, context, tangents that mattered]
45+
46+
Rules:
47+
- Be terse. No fluff.
48+
- Preserve specifics: numbers, quotes, technical terms
49+
- If you're uncertain about something, say "unclear from transcript: [thing]"
50+
- Don't invent structure that isn't there - if there were no action items, say "no explicit action items"
51+
- Prioritize what's ACTIONABLE or DECIDABLE over background chatter
52+
53+
Transcript:`;

src/renderer/features/recordings/components/RecordingDetail.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import type { Recording } from "@shared/types";
1515
import { format } from "date-fns";
1616
import { useHotkeys } from "react-hotkeys-hook";
17+
import { MarkdownRenderer } from "../../../components/MarkdownRenderer";
1718
import { useAuthStore } from "../../../stores/authStore";
1819
import { useRecordingStore } from "../stores/recordingStore";
1920
import { AudioPlayer } from "./AudioPlayer";
@@ -137,6 +138,45 @@ export function RecordingDetail({
137138
</>
138139
)}
139140

141+
{recording.transcription?.notes && (
142+
<>
143+
<Separator size="4" />
144+
<Flex direction="column" gap="3">
145+
<Heading size="4">Notes</Heading>
146+
<Card>
147+
<Box
148+
p="3"
149+
style={{
150+
fontSize: "var(--font-size-2)",
151+
lineHeight: "1.6",
152+
}}
153+
className="notes-section"
154+
>
155+
<style>{`
156+
.notes-section h2 {
157+
font-size: var(--font-size-4) !important;
158+
font-weight: 600 !important;
159+
margin-top: 1rem !important;
160+
margin-bottom: 0.5rem !important;
161+
color: var(--gray-12) !important;
162+
}
163+
.notes-section h2:first-child {
164+
margin-top: 0 !important;
165+
}
166+
.notes-section ul {
167+
margin-bottom: 0.75rem !important;
168+
}
169+
.notes-section li {
170+
margin-bottom: 0.25rem !important;
171+
}
172+
`}</style>
173+
<MarkdownRenderer content={recording.transcription.notes} />
174+
</Box>
175+
</Card>
176+
</Flex>
177+
</>
178+
)}
179+
140180
{recording.transcription?.text && (
141181
<>
142182
<Separator size="4" />

src/shared/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export interface Recording {
158158
status: "processing" | "completed" | "error";
159159
text: string;
160160
summary?: string;
161+
notes?: string; // Structured meeting notes in markdown
161162
extracted_tasks?: Array<{
162163
title: string;
163164
description: string;

0 commit comments

Comments
 (0)