Skip to content

Commit 8344738

Browse files
committed
refactor: audio recording, processing, logging & app lifecyle
1 parent 383a738 commit 8344738

52 files changed

Lines changed: 2112 additions & 2361 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ yarn-error.log*
3838
*.pem
3939
CLAUDE.md
4040
.serena
41+
.local
4142

4243
# Temp files
4344
/tmp

apps/desktop/forge.env.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
/// <reference types="@electron-forge/plugin-vite/forge-vite-env" />
2+
3+
declare module "*?url" {
4+
const url: string;
5+
export default url;
6+
}

apps/desktop/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@dnd-kit/utilities": "^3.2.2",
6060
"@hookform/resolvers": "^5.0.1",
6161
"@libsql/client": "^0.15.9",
62+
"@openrouter/ai-sdk-provider": "^0.7.2",
6263
"@radix-ui/react-accordion": "^1.2.10",
6364
"@radix-ui/react-alert-dialog": "^1.1.13",
6465
"@radix-ui/react-aspect-ratio": "^1.1.6",
@@ -95,6 +96,7 @@
9596
"@types/split2": "^4.2.3",
9697
"@types/uuid": "^10.0.0",
9798
"ai": "^4.3.16",
99+
"ansi-colors": "^4.1.3",
98100
"async-mutex": "^0.5.0",
99101
"class-variance-authority": "^0.7.1",
100102
"clsx": "^2.1.1",

apps/desktop/src/db/config.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export const db = drizzle(`file:${dbPath}`, {
1717
// Initialize database with migrations
1818
let isInitialized = false;
1919

20+
import { logger } from "../main/logger";
21+
2022
export async function initializeDatabase() {
2123
if (isInitialized) {
2224
return;
@@ -35,10 +37,10 @@ export async function initializeDatabase() {
3537
migrationsPath = path.join(process.resourcesPath, "migrations");
3638
}
3739

38-
console.log("Attempting to run migrations from:", migrationsPath);
39-
console.log("__dirname:", __dirname);
40-
console.log("process.cwd():", process.cwd());
41-
console.log("isDev:", isDev);
40+
logger.db.debug("Attempting to run migrations from:", migrationsPath);
41+
logger.db.debug("__dirname:", __dirname);
42+
logger.db.debug("process.cwd():", process.cwd());
43+
logger.db.debug("isDev:", isDev);
4244

4345
// Check if the migrations path exists
4446
if (!fs.existsSync(migrationsPath)) {
@@ -55,11 +57,13 @@ export async function initializeDatabase() {
5557
migrationsFolder: migrationsPath,
5658
});
5759

58-
console.log("Database initialized and migrations completed successfully");
60+
logger.db.info(
61+
"Database initialized and migrations completed successfully",
62+
);
5963
isInitialized = true;
6064
} catch (error) {
61-
console.error("FATAL: Error initializing database:", error);
62-
console.error(
65+
logger.db.error("FATAL: Error initializing database:", error);
66+
logger.db.error(
6367
"Application cannot continue without a working database. Exiting...",
6468
);
6569

apps/desktop/src/db/downloaded-models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export async function deleteDownloadedModel(id: string) {
7575
return result[0] || null;
7676
}
7777

78-
// Get downloaded models as a record (for backward compatibility)
78+
// Get downloaded models as a record
7979
export async function getDownloadedModelsRecord(): Promise<
8080
Record<string, DownloadedModel>
8181
> {

apps/desktop/src/db/migrate.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { migrate } from "drizzle-orm/libsql/migrator";
22
import { db } from "./config";
3+
import { logger } from "../main/logger";
34

45
export async function runMigrations() {
56
try {
67
// Run migrations
78
await migrate(db, { migrationsFolder: "./src/db/migrations" });
8-
console.log("Migrations completed successfully");
9+
logger.db.info("Migrations completed successfully");
910
} catch (error) {
10-
console.error("Error running migrations:", error);
11+
logger.db.error("Error running migrations:", error);
1112
throw error;
1213
}
1314
}

apps/desktop/public/audio-recorder-worklet.js renamed to apps/desktop/src/hooks/audio-recorder-worklet.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// AudioWorklet processor source code
2+
export const audioRecorderWorkletSource = `
13
// AudioWorklet processor for real-time audio capture
24
// This runs in the audio rendering thread for low-latency processing
35
/* eslint-env worker */
@@ -60,3 +62,4 @@ class AudioRecorderProcessor extends AudioWorkletProcessor {
6062
6163
// Register the processor
6264
registerProcessor('audio-recorder-processor', AudioRecorderProcessor);
65+
`;

apps/desktop/src/hooks/useRecording.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useEffect, useRef, useCallback } from "react";
22
import { MicVAD } from "@ricky0123/vad-web";
33
import { Mutex } from "async-mutex";
4+
import { audioRecorderWorkletSource } from "./audio-recorder-worklet";
45

56
export interface UseRecordingParams {
67
onAudioChunk: (
@@ -72,19 +73,6 @@ export const useRecording = ({
7273
"Hook: Internal: Stopping recording and sending final chunk...",
7374
);
7475

75-
// Send final audio chunk before cleanup
76-
try {
77-
// Access the sendAudioChunk function from the current recording session
78-
// We need to store this reference when starting recording
79-
const sendFinalChunk = (window as any).currentSendAudioChunk;
80-
if (sendFinalChunk) {
81-
await sendFinalChunk(true); // Send final chunk
82-
console.log("Hook: Final audio chunk sent.");
83-
}
84-
} catch (error) {
85-
console.error("Hook: Error sending final audio chunk:", error);
86-
}
87-
8876
// Cleanup all resources
8977
cleanupMediaResources(vadRef.current, streamRef.current);
9078

@@ -148,8 +136,13 @@ export const useRecording = ({
148136
let chunkTimer: NodeJS.Timeout | null = null;
149137
let pendingAudioChunks: Float32Array[] = [];
150138

151-
// Load AudioWorklet module
152-
await audioContext.audioWorklet.addModule("/audio-recorder-worklet.js");
139+
// Load AudioWorklet module using blob URL
140+
const blob = new Blob([audioRecorderWorkletSource], {
141+
type: "application/javascript",
142+
});
143+
const audioWorkletUrl = URL.createObjectURL(blob);
144+
await audioContext.audioWorklet.addModule(audioWorkletUrl);
145+
URL.revokeObjectURL(audioWorkletUrl); // Clean up blob URL
153146
console.log("Hook: AudioWorklet module loaded successfully");
154147

155148
source = audioContext.createMediaStreamSource(localStream);
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import {
2+
app,
3+
systemPreferences,
4+
BrowserWindow,
5+
globalShortcut,
6+
} from "electron";
7+
import { initializeDatabase } from "../../db/config";
8+
import { logger } from "../logger";
9+
import { WindowManager } from "./window-manager";
10+
import { setupApplicationMenu } from "../menu";
11+
import { ServiceManager } from "../managers/service-manager";
12+
import { createIPCHandler } from "electron-trpc-experimental/main";
13+
import { router } from "../../trpc/router";
14+
import { EventHandlers } from "./event-handlers";
15+
16+
export class AppManager {
17+
private windowManager: WindowManager;
18+
private serviceManager: ServiceManager;
19+
20+
constructor() {
21+
this.windowManager = new WindowManager();
22+
this.serviceManager = ServiceManager.createInstance();
23+
this.windowManager.setMainWindowCreatedCallback(
24+
this.onMainWindowCreated.bind(this),
25+
);
26+
}
27+
28+
async initialize(): Promise<void> {
29+
try {
30+
await this.initializeDatabase();
31+
await this.requestPermissions();
32+
await this.serviceManager.initialize(this.windowManager);
33+
this.exposeGlobalServices();
34+
await this.setupWindows();
35+
await this.setupMenu();
36+
37+
// Setup event handlers
38+
const eventHandlers = new EventHandlers(this);
39+
eventHandlers.setupEventHandlers();
40+
41+
// Schedule auto-update check after startup
42+
this.scheduleAutoUpdateCheck();
43+
44+
logger.main.info("Application initialized successfully");
45+
} catch (error) {
46+
logger.main.error("Error initializing app:", error);
47+
throw error;
48+
}
49+
}
50+
51+
private async initializeDatabase(): Promise<void> {
52+
await initializeDatabase();
53+
logger.db.info(
54+
"Database initialized and migrations completed successfully",
55+
);
56+
}
57+
58+
private async requestPermissions(): Promise<void> {
59+
if (process.platform === "darwin") {
60+
const accessibilityEnabled =
61+
systemPreferences.isTrustedAccessibilityClient(false);
62+
if (!accessibilityEnabled) {
63+
logger.main.debug(
64+
"Please enable accessibility permissions in System Preferences > Security & Privacy > Privacy > Accessibility",
65+
);
66+
}
67+
}
68+
69+
const microphoneEnabled =
70+
systemPreferences.getMediaAccessStatus("microphone");
71+
logger.main.info("Microphone access status:", {
72+
status: microphoneEnabled,
73+
});
74+
75+
if (microphoneEnabled !== "granted") {
76+
await systemPreferences.askForMediaAccess("microphone");
77+
}
78+
}
79+
80+
private async setupWindows(): Promise<void> {
81+
this.windowManager.createWidgetWindow();
82+
this.setupTRPCHandler();
83+
84+
if (process.platform === "darwin" && app.dock) {
85+
app.dock.show();
86+
}
87+
}
88+
89+
private setupTRPCHandler(): Promise<void> {
90+
const windows = this.windowManager
91+
.getAllWindows()
92+
.filter((w): w is BrowserWindow => w !== null);
93+
createIPCHandler({ router, windows });
94+
return Promise.resolve();
95+
}
96+
97+
updateTRPCHandler(): void {
98+
const windows = this.windowManager
99+
.getAllWindows()
100+
.filter((w): w is BrowserWindow => w !== null);
101+
createIPCHandler({ router, windows });
102+
}
103+
104+
private async setupMenu(): Promise<void> {
105+
setupApplicationMenu(
106+
() => this.windowManager.createOrShowMainWindow(),
107+
() => {
108+
const autoUpdaterService = this.serviceManager.getAutoUpdaterService();
109+
if (autoUpdaterService) {
110+
autoUpdaterService.checkForUpdates(true);
111+
}
112+
},
113+
() => this.windowManager.openAllDevTools(),
114+
);
115+
}
116+
117+
private exposeGlobalServices(): void {
118+
// Make services available globally for tRPC (temporary solution)
119+
const transcriptionService = this.serviceManager.getTranscriptionService();
120+
const autoUpdaterService = this.serviceManager.getAutoUpdaterService();
121+
const settingsService = this.serviceManager.getSettingsService();
122+
const swiftBridge = this.serviceManager.getSwiftIOBridge();
123+
124+
(globalThis as any).modelManagerService =
125+
this.serviceManager.getModelManagerService();
126+
(globalThis as any).transcriptionService = transcriptionService;
127+
(globalThis as any).settingsService = settingsService;
128+
(globalThis as any).logger = logger;
129+
(globalThis as any).autoUpdaterService = autoUpdaterService;
130+
(globalThis as any).swiftBridge = swiftBridge;
131+
}
132+
133+
getWindowManager(): WindowManager {
134+
return this.windowManager;
135+
}
136+
137+
getServiceManager(): ServiceManager {
138+
return this.serviceManager;
139+
}
140+
141+
getTranscriptionService(): any {
142+
return this.serviceManager.getTranscriptionService();
143+
}
144+
145+
getSwiftIOBridge(): any {
146+
return this.serviceManager.getSwiftIOBridge();
147+
}
148+
149+
getAutoUpdaterService(): any {
150+
return this.serviceManager.getAutoUpdaterService();
151+
}
152+
153+
private scheduleAutoUpdateCheck(): void {
154+
// Check for updates on startup (after a brief delay)
155+
setTimeout(() => {
156+
try {
157+
const autoUpdaterService = this.serviceManager.getAutoUpdaterService();
158+
autoUpdaterService.checkForUpdatesAndNotify();
159+
} catch (error) {
160+
logger.main.warn("Auto-update check failed during startup", {
161+
error: error instanceof Error ? error.message : String(error),
162+
});
163+
}
164+
}, 5000); // Wait 5 seconds after startup
165+
}
166+
167+
private onMainWindowCreated(window: BrowserWindow): void {
168+
this.updateTRPCHandler();
169+
}
170+
171+
async cleanup(): Promise<void> {
172+
globalShortcut.unregisterAll();
173+
await this.serviceManager.cleanup();
174+
if (this.windowManager) {
175+
this.windowManager.cleanup();
176+
}
177+
}
178+
179+
handleActivate(): void {
180+
const allWindows = this.windowManager.getAllWindows();
181+
182+
if (allWindows.every((w) => !w || w.isDestroyed())) {
183+
this.windowManager.createWidgetWindow();
184+
} else {
185+
const widgetWindow = this.windowManager.getWidgetWindow();
186+
if (!widgetWindow || widgetWindow.isDestroyed()) {
187+
this.windowManager.createWidgetWindow();
188+
} else {
189+
widgetWindow.show();
190+
}
191+
this.windowManager.createOrShowMainWindow();
192+
}
193+
}
194+
}

0 commit comments

Comments
 (0)