Skip to content

Commit f695f98

Browse files
committed
refactor and ready to review
1 parent 5b5382b commit f695f98

File tree

14 files changed

+934
-587
lines changed

14 files changed

+934
-587
lines changed

src/main/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ function createWindow(): void {
6767
nodeIntegration: false,
6868
contextIsolation: true,
6969
preload: path.join(__dirname, "preload.js"),
70+
enableBlinkFeatures: "GetDisplayMedia",
7071
},
7172
});
7273

src/main/services/recording.ts

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@ import {
1212
TASK_EXTRACTION_PROMPT,
1313
} from "./transcription-prompts.js";
1414

15-
// Polyfill File for Node.js compatibility
1615
let FileConstructor: typeof File;
1716
try {
18-
// Try importing from node:buffer (Node 20+)
1917
const { File: NodeFile } = await import("node:buffer");
2018
FileConstructor = NodeFile as typeof File;
2119
} catch {
22-
// Fallback for Node < 20
2320
FileConstructor = class File extends Blob {
2421
name: string;
2522
lastModified: number;
@@ -44,7 +41,6 @@ interface RecordingSession {
4441
const activeRecordings = new Map<string, RecordingSession>();
4542
const recordingsDir = path.join(app.getPath("userData"), "recordings");
4643

47-
// Ensure recordings directory exists
4844
if (!fs.existsSync(recordingsDir)) {
4945
fs.mkdirSync(recordingsDir, { recursive: true });
5046
}
@@ -53,21 +49,16 @@ if (!fs.existsSync(recordingsDir)) {
5349
* Validates a recording ID to prevent path traversal attacks
5450
*/
5551
function validateRecordingId(recordingId: string): boolean {
56-
// Only allow alphanumeric characters, dots, hyphens, and underscores
5752
const safePattern = /^[a-zA-Z0-9._-]+$/;
5853
if (!safePattern.test(recordingId)) {
5954
return false;
6055
}
6156

62-
// Ensure the resolved path stays within the recordings directory
6357
const resolvedPath = path.resolve(path.join(recordingsDir, recordingId));
6458
const recordingsDirResolved = path.resolve(recordingsDir);
6559
return resolvedPath.startsWith(recordingsDirResolved + path.sep);
6660
}
6761

68-
/**
69-
* Generate a summary title for a transcript using GPT with structured output
70-
*/
7162
async function generateTranscriptSummary(
7263
transcriptText: string,
7364
openaiApiKey: string,
@@ -100,9 +91,6 @@ async function generateTranscriptSummary(
10091
}
10192
}
10293

103-
/**
104-
* Extract actionable tasks from a transcript using GPT with structured output
105-
*/
10694
async function extractTasksFromTranscript(
10795
transcriptText: string,
10896
openaiApiKey: string,
@@ -146,12 +134,11 @@ function safeLog(...args: unknown[]): void {
146134
try {
147135
console.log(...args);
148136
} catch {
149-
// Ignore logging errors (e.g., write EIO during startup)
137+
// Ignore logging errors
150138
}
151139
}
152140

153141
export function registerRecordingIpc(): void {
154-
// Desktop capturer for system audio
155142
ipcMain.handle(
156143
"desktop-capturer:get-sources",
157144
async (_event, options: { types: ("screen" | "window")[] }) => {
@@ -194,16 +181,13 @@ export function registerRecordingIpc(): void {
194181
throw new Error("Recording session not found");
195182
}
196183

197-
// Save the recording
198184
const filename = `recording-${session.startTime.toISOString().replace(/[:.]/g, "-")}.webm`;
199185
const filePath = path.join(recordingsDir, filename);
200186
const metadataPath = path.join(recordingsDir, `${filename}.json`);
201187

202-
// Save the audio data
203188
const buffer = Buffer.from(audioData);
204189
fs.writeFileSync(filePath, buffer);
205190

206-
// Save metadata
207191
const metadata = {
208192
duration,
209193
created_at: session.startTime.toISOString(),
@@ -243,7 +227,6 @@ export function registerRecordingIpc(): void {
243227
let createdAt = new Date().toISOString();
244228
let transcription: Recording["transcription"];
245229

246-
// Try to read metadata
247230
if (fs.existsSync(metadataPath)) {
248231
try {
249232
const metadataContent = fs.readFileSync(metadataPath, "utf-8");
@@ -278,6 +261,13 @@ export function registerRecordingIpc(): void {
278261
}
279262

280263
const filePath = path.join(recordingsDir, recordingId);
264+
265+
const resolvedPath = fs.realpathSync.native(filePath);
266+
const recordingsDirResolved = fs.realpathSync.native(recordingsDir);
267+
if (!resolvedPath.startsWith(recordingsDirResolved)) {
268+
throw new Error("Invalid recording path");
269+
}
270+
281271
const metadataPath = path.join(recordingsDir, `${recordingId}.json`);
282272

283273
let deleted = false;
@@ -305,6 +295,12 @@ export function registerRecordingIpc(): void {
305295
throw new Error("Recording file not found");
306296
}
307297

298+
const resolvedPath = fs.realpathSync.native(filePath);
299+
const recordingsDirResolved = fs.realpathSync.native(recordingsDir);
300+
if (!resolvedPath.startsWith(recordingsDirResolved)) {
301+
throw new Error("Invalid recording path");
302+
}
303+
308304
const buffer = fs.readFileSync(filePath);
309305
return buffer;
310306
});
@@ -323,7 +319,12 @@ export function registerRecordingIpc(): void {
323319
throw new Error("Recording file not found");
324320
}
325321

326-
// Update metadata to show processing
322+
const resolvedPath = fs.realpathSync.native(filePath);
323+
const recordingsDirResolved = fs.realpathSync.native(recordingsDir);
324+
if (!resolvedPath.startsWith(recordingsDirResolved)) {
325+
throw new Error("Invalid recording path");
326+
}
327+
327328
let metadata: Record<string, unknown> = {};
328329
if (fs.existsSync(metadataPath)) {
329330
metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
@@ -338,16 +339,14 @@ export function registerRecordingIpc(): void {
338339
try {
339340
const openai = createOpenAI({ apiKey: openaiApiKey });
340341

341-
// Read the file
342342
const audio = await readFile(filePath);
343-
const maxSize = 25 * 1024 * 1024; // 25 MB limit
343+
const maxSize = 25 * 1024 * 1024;
344344
const fileSize = audio.length;
345345

346346
safeLog(
347347
`[Transcription] Starting (${(fileSize / 1024 / 1024).toFixed(2)} MB)`,
348348
);
349349

350-
// Check file size
351350
if (fileSize > maxSize) {
352351
throw new Error(
353352
`Recording file is too large (${(fileSize / 1024 / 1024).toFixed(1)} MB). ` +
@@ -356,7 +355,6 @@ export function registerRecordingIpc(): void {
356355
);
357356
}
358357

359-
// Call OpenAI transcription with detailed output
360358
const result = await transcribe({
361359
model: openai.transcription("gpt-4o-mini-transcribe"),
362360
audio,
@@ -366,13 +364,11 @@ export function registerRecordingIpc(): void {
366364

367365
const fullTranscriptText = result.text;
368366

369-
// Generate summary title
370367
const summaryTitle = await generateTranscriptSummary(
371368
fullTranscriptText,
372369
openaiApiKey,
373370
);
374371

375-
// Extract actionable tasks using GPT
376372
const extractedTasks = await extractTasksFromTranscript(
377373
fullTranscriptText,
378374
openaiApiKey,
@@ -382,7 +378,6 @@ export function registerRecordingIpc(): void {
382378
`[Transcription] Complete - ${extractedTasks.length} tasks extracted`,
383379
);
384380

385-
// Update metadata with transcription, summary, and extracted tasks
386381
metadata.transcription = {
387382
status: "completed",
388383
text: fullTranscriptText,
@@ -400,7 +395,6 @@ export function registerRecordingIpc(): void {
400395
} catch (error) {
401396
console.error("[Transcription] Error:", error);
402397

403-
// Update metadata with error
404398
metadata.transcription = {
405399
status: "error",
406400
text: "",

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

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Pause, Play } from "@phosphor-icons/react";
2-
import { Box, Button, Flex, Slider, Text } from "@radix-ui/themes";
1+
import { FastForward, Pause, Play, Rewind } from "@phosphor-icons/react";
2+
import { Box, Button, Flex, Text } from "@radix-ui/themes";
33
import { useCallback, useEffect, useRef, useState } from "react";
4+
import { useHotkeys } from "react-hotkeys-hook";
45

56
interface AudioPlayerProps {
67
recordingId: string;
@@ -21,7 +22,6 @@ export function AudioPlayer({ recordingId, duration }: AudioPlayerProps) {
2122
const audioRef = useRef<HTMLAudioElement | null>(null);
2223
const audioUrlRef = useRef<string | null>(null);
2324

24-
// Load audio file and initialize audio element
2525
useEffect(() => {
2626
let mounted = true;
2727
setIsReady(false);
@@ -97,13 +97,6 @@ export function AudioPlayer({ recordingId, duration }: AudioPlayerProps) {
9797
}
9898
}, [isPlaying]);
9999

100-
const handleSeek = useCallback((value: number[]) => {
101-
if (!audioRef.current) return;
102-
const newTime = value[0];
103-
audioRef.current.currentTime = newTime;
104-
setCurrentTime(newTime);
105-
}, []);
106-
107100
const cyclePlaybackRate = useCallback(() => {
108101
if (!audioRef.current) return;
109102
const rates = [1, 1.25, 1.5, 2];
@@ -113,11 +106,48 @@ export function AudioPlayer({ recordingId, duration }: AudioPlayerProps) {
113106
setPlaybackRate(nextRate);
114107
}, [playbackRate]);
115108

109+
const skipBackward = useCallback(() => {
110+
if (!audioRef.current) return;
111+
audioRef.current.currentTime = Math.max(
112+
0,
113+
audioRef.current.currentTime - 10,
114+
);
115+
}, []);
116+
117+
const skipForward = useCallback(() => {
118+
if (!audioRef.current) return;
119+
audioRef.current.currentTime = Math.min(
120+
duration,
121+
audioRef.current.currentTime + 10,
122+
);
123+
}, [duration]);
124+
125+
useHotkeys(
126+
"space",
127+
(e) => {
128+
e.preventDefault();
129+
togglePlayPause();
130+
},
131+
{ enableOnFormTags: false },
132+
[togglePlayPause],
133+
);
134+
116135
return (
117-
<Flex direction="column" gap="2">
136+
<Flex direction="column" gap="3">
118137
<Flex align="center" gap="2">
119138
<Button
120139
size="1"
140+
variant="ghost"
141+
color="gray"
142+
onClick={skipBackward}
143+
disabled={!isReady}
144+
title="Skip backward 10s"
145+
>
146+
<Rewind weight="fill" size={16} />
147+
</Button>
148+
149+
<Button
150+
size="2"
121151
variant="soft"
122152
color={isPlaying ? "blue" : "gray"}
123153
onClick={togglePlayPause}
@@ -126,15 +156,18 @@ export function AudioPlayer({ recordingId, duration }: AudioPlayerProps) {
126156
{isPlaying ? <Pause weight="fill" /> : <Play weight="fill" />}
127157
</Button>
128158

129-
<Box style={{ flex: 1 }}>
130-
<Slider
131-
value={[currentTime]}
132-
max={duration || 100}
133-
step={0.1}
134-
onValueChange={handleSeek}
135-
disabled={!isReady}
136-
/>
137-
</Box>
159+
<Button
160+
size="1"
161+
variant="ghost"
162+
color="gray"
163+
onClick={skipForward}
164+
disabled={!isReady}
165+
title="Skip forward 10s"
166+
>
167+
<FastForward weight="fill" size={16} />
168+
</Button>
169+
170+
<Box style={{ flex: 1 }} />
138171

139172
<Flex gap="2" align="center">
140173
<Text

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

Lines changed: 0 additions & 75 deletions
This file was deleted.

0 commit comments

Comments
 (0)