chore: enable audio downloading

This commit is contained in:
Naomi Chopra 2025-07-04 14:22:28 +05:30 committed by haritabh-z01
parent 64d9e4fc71
commit 8ead8d1454
10 changed files with 494 additions and 79 deletions

View file

@ -1,16 +1,9 @@
import {
app,
systemPreferences,
BrowserWindow,
globalShortcut,
} from "electron";
import { app, systemPreferences, globalShortcut } from "electron";
import { initializeDatabase } from "../../db/config";
import { logger } from "../logger";
import { WindowManager } from "./window-manager";
import { setupApplicationMenu } from "../menu";
import { ServiceManager } from "../managers/service-manager";
import { createIPCHandler } from "electron-trpc-experimental/main";
import { router } from "../../trpc/router";
import { EventHandlers } from "./event-handlers";
export class AppManager {
@ -20,9 +13,6 @@ export class AppManager {
constructor() {
this.windowManager = new WindowManager();
this.serviceManager = ServiceManager.createInstance();
this.windowManager.setMainWindowCreatedCallback(
this.onMainWindowCreated.bind(this),
);
}
async initialize(): Promise<void> {
@ -30,7 +20,7 @@ export class AppManager {
await this.initializeDatabase();
await this.requestPermissions();
await this.serviceManager.initialize(this.windowManager);
await this.serviceManager.initialize();
this.exposeGlobalServices();
await this.setupWindows();
await this.setupMenu();
@ -80,28 +70,13 @@ export class AppManager {
private async setupWindows(): Promise<void> {
this.windowManager.createWidgetWindow();
this.windowManager.createOrShowMainWindow();
this.setupTRPCHandler();
// tRPC handler is now set up in WindowManager when windows are created
if (process.platform === "darwin" && app.dock) {
app.dock.show();
}
}
private setupTRPCHandler(): Promise<void> {
const windows = this.windowManager
.getAllWindows()
.filter((w): w is BrowserWindow => w !== null);
createIPCHandler({ router, windows });
return Promise.resolve();
}
updateTRPCHandler(): void {
const windows = this.windowManager
.getAllWindows()
.filter((w): w is BrowserWindow => w !== null);
createIPCHandler({ router, windows });
}
private async setupMenu(): Promise<void> {
setupApplicationMenu(
() => this.windowManager.createOrShowMainWindow(),
@ -151,10 +126,6 @@ export class AppManager {
return this.serviceManager.getAutoUpdaterService();
}
private onMainWindowCreated(window: BrowserWindow): void {
this.updateTRPCHandler();
}
async cleanup(): Promise<void> {
globalShortcut.unregisterAll();
await this.serviceManager.cleanup();

View file

@ -1,6 +1,7 @@
import { BrowserWindow, screen, systemPreferences } from "electron";
import path from "node:path";
import { logger } from "../logger";
import { ServiceManager } from "../managers/service-manager";
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
declare const MAIN_WINDOW_VITE_NAME: string;
@ -11,7 +12,6 @@ export class WindowManager {
private widgetWindow: BrowserWindow | null = null;
private currentWindowDisplayId: number | null = null;
private activeSpaceChangeSubscriptionId: number | null = null;
private onMainWindowCreated?: (window: BrowserWindow) => void;
createOrShowMainWindow(): void {
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
@ -43,12 +43,15 @@ export class WindowManager {
}
this.mainWindow.on("closed", () => {
ServiceManager.getInstance()!
.getTRPCHandler()!
.detachWindow(this.mainWindow!);
this.mainWindow = null;
});
if (this.onMainWindowCreated) {
this.onMainWindowCreated(this.mainWindow);
}
ServiceManager.getInstance()!
.getTRPCHandler()!
.attachWindow(this.mainWindow!);
}
createWidgetWindow(): void {
@ -89,6 +92,13 @@ export class WindowManager {
);
}
this.widgetWindow.on("closed", () => {
ServiceManager.getInstance()!
.getTRPCHandler()!
.detachWindow(this.widgetWindow!);
this.widgetWindow = null;
});
if (process.platform === "darwin") {
this.widgetWindow.setAlwaysOnTop(true, "floating", 1);
this.widgetWindow.setVisibleOnAllWorkspaces(true, {
@ -97,6 +107,11 @@ export class WindowManager {
this.widgetWindow.setHiddenInMissionControl(true);
this.setupDisplayChangeNotifications();
}
// Update tRPC handler with new window
ServiceManager.getInstance()!
.getTRPCHandler()!
.attachWindow(this.widgetWindow!);
}
private setupDisplayChangeNotifications(): void {
@ -159,12 +174,6 @@ export class WindowManager {
return [this.mainWindow, this.widgetWindow];
}
setMainWindowCreatedCallback(
callback: (window: BrowserWindow) => void,
): void {
this.onMainWindowCreated = callback;
}
openAllDevTools(): void {
const windows = this.getAllWindows().filter(
(window): window is BrowserWindow =>

View file

@ -4,7 +4,6 @@ import { logger, logPerformance } from "../logger";
import { ServiceManager } from "./service-manager";
import { appContextStore } from "../../stores/app-context";
import type { RecordingState, RecordingStatus } from "../../types/recording";
import { WindowManager } from "../core/window-manager";
/**
* Manages recording state and coordinates audio recording across the application
@ -14,17 +13,12 @@ export class RecordingManager extends EventEmitter {
private currentSessionId: string | null = null;
private recordingState: RecordingState = "idle";
private lastError: string | undefined;
private windowManager: WindowManager | null = null;
constructor(private serviceManager: ServiceManager) {
super();
this.setupIPCHandlers();
}
public setWindowManager(windowManager: WindowManager): void {
this.windowManager = windowManager;
}
private setState(newState: RecordingState, error?: string): void {
const oldState = this.recordingState;
this.recordingState = newState;

View file

@ -4,9 +4,11 @@ import { TranscriptionService } from "../../services/transcription-service";
import { SettingsService } from "../../services/settings-service";
import { SwiftIOBridge } from "../../services/platform/swift-bridge-service";
import { AutoUpdaterService } from "../services/auto-updater";
import { WindowManager } from "../core/window-manager";
import { RecordingManager } from "./recording-manager";
import { VADService } from "../../services/vad-service";
import { createIPCHandler } from "electron-trpc-experimental/main";
import { router } from "../../trpc/router";
import { BrowserWindow } from "electron";
/**
* Manages service initialization and lifecycle
@ -23,8 +25,9 @@ export class ServiceManager {
private swiftIOBridge: SwiftIOBridge | null = null;
private autoUpdaterService: AutoUpdaterService | null = null;
private recordingManager: RecordingManager | null = null;
private trpcHandler: ReturnType<typeof createIPCHandler> | null = null;
async initialize(windowManager: WindowManager): Promise<void> {
async initialize(): Promise<void> {
if (this.isInitialized) {
logger.main.warn(
"ServiceManager is already initialized, skipping initialization",
@ -38,8 +41,9 @@ export class ServiceManager {
this.initializePlatformServices();
await this.initializeVADService();
await this.initializeAIServices();
this.initializeRecordingManager(windowManager);
this.initializeAutoUpdater(windowManager);
this.initializeRecordingManager();
this.initializeAutoUpdater();
this.initializeTRPCHandler();
this.isInitialized = true;
logger.main.info("Services initialized successfully");
@ -125,14 +129,19 @@ export class ServiceManager {
}
}
private initializeRecordingManager(windowManager: WindowManager): void {
private initializeRecordingManager(): void {
this.recordingManager = new RecordingManager(this);
this.recordingManager.setWindowManager(windowManager);
logger.main.info("Recording manager initialized");
}
private initializeAutoUpdater(windowManager: WindowManager): void {
this.autoUpdaterService = new AutoUpdaterService(windowManager);
private initializeAutoUpdater(): void {
this.autoUpdaterService = new AutoUpdaterService();
}
private initializeTRPCHandler(): void {
// Initialize with empty windows array, windows will be added later
this.trpcHandler = createIPCHandler({ router, windows: [] });
logger.main.info("tRPC handler initialized");
}
// Getters for other managers to access services
@ -217,6 +226,18 @@ export class ServiceManager {
return this.vadService;
}
getTRPCHandler(): ReturnType<typeof createIPCHandler> | null {
if (!this.isInitialized) {
throw new Error(
"ServiceManager not initialized. Call initialize() first.",
);
}
if (!this.trpcHandler) {
throw new Error("TRPCHandler failed to initialize");
}
return this.trpcHandler;
}
async cleanup(): Promise<void> {
if (this.recordingManager) {
logger.main.info("Cleaning up recording manager...");

View file

@ -1,10 +1,9 @@
import { app } from "electron";
import { EventEmitter } from "events";
import { logger } from "../logger";
import { WindowManager } from "../core/window-manager";
export class AutoUpdaterService extends EventEmitter {
constructor(private windowManager: WindowManager) {
constructor() {
super();
}

View file

@ -19,6 +19,7 @@ import {
FileText,
Search,
MoreHorizontal,
FileAudio,
} from "lucide-react";
import { format } from "date-fns";
import { Input } from "@/components/ui/input";
@ -33,6 +34,7 @@ import {
export const TranscriptionsList: React.FC = () => {
const [searchTerm, setSearchTerm] = useState("");
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
// tRPC React Query hooks
const transcriptionsQuery = api.transcriptions.getTranscriptions.useQuery(
@ -72,6 +74,13 @@ export const TranscriptionsList: React.FC = () => {
},
});
const downloadAudioMutation =
api.transcriptions.downloadAudioFile.useMutation({
onError: (error) => {
console.error("Error downloading audio:", error);
},
});
const transcriptions = transcriptionsQuery.data || [];
const totalCount = transcriptionsCountQuery.data || 0;
const loading =
@ -95,15 +104,19 @@ export const TranscriptionsList: React.FC = () => {
console.log("Playing audio:", audioFile);
};
const handleDownload = (transcription: Transcription) => {
// Create and download a text file with the transcription
const element = document.createElement("a");
const file = new Blob([transcription.text], { type: "text/plain" });
element.href = URL.createObjectURL(file);
element.download = `transcription-${transcription.id}.txt`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
const handleDownloadAudio = async (transcriptionId: number) => {
console.log("Downloading audio:", transcriptionId);
// Close dropdown first
setOpenDropdownId(null);
// Small delay to ensure dropdown closes before system dialog opens
setTimeout(async () => {
try {
await downloadAudioMutation.mutateAsync({ transcriptionId });
} catch (error) {
console.error("Failed to download audio:", error);
}
}, 0);
};
const getTitle = (text: string) => {
@ -227,7 +240,12 @@ export const TranscriptionsList: React.FC = () => {
</TooltipProvider>
)}
<DropdownMenu>
<DropdownMenu
open={openDropdownId === transcription.id}
onOpenChange={(open) =>
setOpenDropdownId(open ? transcription.id : null)
}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
@ -239,12 +257,17 @@ export const TranscriptionsList: React.FC = () => {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => handleDownload(transcription)}
>
<Download className="h-4 w-4 mr-2" />
Download
</DropdownMenuItem>
{transcription.audioFile && (
<DropdownMenuItem
onClick={() =>
handleDownloadAudio(transcription.id)
}
disabled={downloadAudioMutation.isPending}
>
<FileAudio className="h-4 w-4 mr-2" />
Download Audio
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => handleDelete(transcription.id)}

View file

@ -12,15 +12,25 @@ import { createTranscription } from "../db/transcriptions";
import { logger } from "../main/logger";
import { v4 as uuid } from "uuid";
import { VADService } from "./vad-service";
import { app } from "electron";
import * as fs from "node:fs";
import * as path from "node:path";
import { convertRawToWav } from "../utils/audio-converter";
/**
* Service for audio transcription and optional formatting
*/
interface ExtendedStreamingSession extends StreamingSession {
audioFileStream?: fs.WriteStream;
audioFilePath?: string;
audioBuffers?: Buffer[];
}
export class TranscriptionService {
private whisperProvider: WhisperProvider;
private openRouterProvider: OpenRouterProvider | null = null;
private formatterEnabled = false;
private streamingSessions: Map<string, StreamingSession> = new Map();
private streamingSessions: Map<string, ExtendedStreamingSession> = new Map();
private vadService: VADService | null = null;
constructor(
@ -67,6 +77,26 @@ export class TranscriptionService {
}
}
/**
* Create audio file for recording session
*/
private async createAudioFile(sessionId: string): Promise<string> {
// Create audio directory in app temp path
const audioDir = path.join(app.getPath("temp"), "amical-audio");
await fs.promises.mkdir(audioDir, { recursive: true });
// Create file path
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filePath = path.join(audioDir, `audio-${sessionId}-${timestamp}.wav`);
logger.transcription.info("Created audio file for session", {
sessionId,
filePath,
});
return filePath;
}
/**
* Process a single audio chunk in streaming mode
*/
@ -113,13 +143,26 @@ export class TranscriptionService {
streamingContext.sharedData.accessibilityContext =
appContextStore.getAccessibilityContext();
// Create audio file for this session
const audioFilePath = await this.createAudioFile(sessionId);
session = {
context: streamingContext,
transcriptionResults: [],
audioFilePath,
audioBuffers: [],
};
this.streamingSessions.set(sessionId, session);
logger.transcription.info("Started streaming session", { sessionId });
logger.transcription.info("Started streaming session", {
sessionId,
audioFilePath,
});
}
// Buffer audio chunks - we'll write WAV at the end
if (audioChunk.length > 0) {
session.audioBuffers!.push(audioChunk);
}
// Process chunk if it has content
@ -167,6 +210,12 @@ export class TranscriptionService {
// If this is the final chunk, flush any remaining audio and apply formatting
if (isFinal) {
if (!session) {
logger.transcription.error("No session found for final chunk", {
sessionId,
});
return "";
}
// Flush any remaining buffered audio in Whisper
if (this.whisperProvider.flush) {
const flushResult = await this.whisperProvider.flush();
@ -188,7 +237,9 @@ export class TranscriptionService {
chunkCount: session.transcriptionResults.length,
});
// Format if enabled
// Format if enabled (currently disabled with && false)
// Commenting out to fix TypeScript errors since this code path is never executed
/*
if (this.formatterEnabled && this.openRouterProvider && false) {
const style =
session.context.sharedData.userPreferences?.formattingStyle;
@ -209,14 +260,48 @@ export class TranscriptionService {
},
});
}
*/
// Convert buffered audio to WAV and save
if (
session.audioBuffers &&
session.audioBuffers.length > 0 &&
session.audioFilePath
) {
// Concatenate all audio buffers
const totalLength = session.audioBuffers.reduce(
(acc, buf) => acc + buf.length,
0,
);
const combinedBuffer = Buffer.concat(session.audioBuffers, totalLength);
// Convert to WAV
const wavBuffer = convertRawToWav(combinedBuffer);
// Write WAV file
await fs.promises.writeFile(session.audioFilePath, wavBuffer);
logger.transcription.info("Saved audio as WAV file", {
sessionId,
filePath: session.audioFilePath,
size: wavBuffer.length,
});
}
// Save directly to database
logger.transcription.info("Saving transcription with audio file", {
sessionId,
audioFilePath: session.audioFilePath,
hasAudioFile: !!session.audioFilePath,
});
await createTranscription({
text: completeTranscription,
language: session.context.sharedData.userPreferences?.language || "en",
duration: session.context.sharedData.audioMetadata?.duration,
speechModel: "whisper-local",
formattingModel: this.formatterEnabled ? "openrouter" : undefined,
audioFile: session.audioFilePath,
meta: {
sessionId,
source: session.context.sharedData.audioMetadata?.source,
@ -233,7 +318,7 @@ export class TranscriptionService {
}
// Return accumulated transcription so far (for UI feedback)
return session.transcriptionResults.join(" ");
return session ? session.transcriptionResults.join(" ") : "";
}
private async buildContext(): Promise<PipelineContext> {

View file

@ -1,6 +1,9 @@
import { initTRPC } from "@trpc/server";
import superjson from "superjson";
import { z } from "zod";
import { dialog } from "electron";
import * as fs from "node:fs";
import * as path from "node:path";
import {
getTranscriptions,
getTranscriptionById,
@ -10,6 +13,8 @@ import {
getTranscriptionsCount,
searchTranscriptions,
} from "../../db/transcriptions.js";
import { logger } from "../../main/logger.js";
import { deleteAudioFile } from "../../utils/audio-file-cleanup.js";
const t = initTRPC.create({
isServer: true,
@ -96,6 +101,113 @@ export const transcriptionsRouter = t.router({
deleteTranscription: t.procedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
return await deleteTranscription(input.id);
// Get transcription to check for audio file
const transcription = await getTranscriptionById(input.id);
// Delete the transcription
const result = await deleteTranscription(input.id);
// Delete associated audio file if it exists
if (transcription?.audioFile) {
try {
await deleteAudioFile(transcription.audioFile);
} catch (error) {
logger.main.warn(
"Failed to delete audio file during transcription deletion",
{
transcriptionId: input.id,
audioFile: transcription.audioFile,
error,
},
);
}
}
return result;
}),
// Get audio file for download
getAudioFile: t.procedure
.input(z.object({ transcriptionId: z.number() }))
.query(async ({ input }) => {
const transcription = await getTranscriptionById(input.transcriptionId);
if (!transcription?.audioFile) {
throw new Error("No audio file associated with this transcription");
}
try {
// Check if file exists
await fs.promises.access(transcription.audioFile);
// Read the file
const audioData = await fs.promises.readFile(transcription.audioFile);
const filename = path.basename(transcription.audioFile);
return {
data: audioData,
filename,
mimeType: "audio/webm",
};
} catch (error) {
logger.main.error("Failed to read audio file", {
transcriptionId: input.transcriptionId,
audioFile: transcription.audioFile,
error,
});
throw new Error("Audio file not found or inaccessible");
}
}),
// Download audio file with save dialog
downloadAudioFile: t.procedure
.input(z.object({ transcriptionId: z.number() }))
.mutation(async ({ input }) => {
console.log("Downloading audio file", input);
const transcription = await getTranscriptionById(input.transcriptionId);
if (!transcription?.audioFile) {
throw new Error("No audio file associated with this transcription");
}
try {
// Read the audio file (already in WAV format)
const audioData = await fs.promises.readFile(transcription.audioFile);
const filename = path.basename(transcription.audioFile);
// Show save dialog
const result = await dialog.showSaveDialog({
defaultPath: filename,
filters: [
{ name: "WAV Audio", extensions: ["wav"] },
{ name: "All Files", extensions: ["*"] },
],
});
if (result.canceled || !result.filePath) {
return { success: false, canceled: true };
}
// Write file to chosen location
await fs.promises.writeFile(result.filePath, audioData);
logger.main.info("Audio file downloaded", {
transcriptionId: input.transcriptionId,
savedTo: result.filePath,
size: audioData.length,
});
return {
success: true,
filePath: result.filePath,
};
} catch (error) {
logger.main.error("Failed to download audio file", {
transcriptionId: input.transcriptionId,
audioFile: transcription.audioFile,
error,
});
throw new Error("Failed to download audio file");
}
}),
});

View file

@ -0,0 +1,77 @@
/**
* Convert raw PCM audio data to WAV format
* @param rawData Raw audio buffer (Float32 PCM)
* @param sampleRate Sample rate (default: 16000)
* @returns WAV file buffer
*/
export function convertRawToWav(
rawData: Buffer,
sampleRate: number = 16000,
): Buffer {
// Convert Float32 buffer to Float32Array
const float32Data = new Float32Array(
rawData.buffer,
rawData.byteOffset,
rawData.length / 4,
);
// Convert Float32 to Int16
const int16Data = new Int16Array(float32Data.length);
for (let i = 0; i < float32Data.length; i++) {
// Clamp to [-1, 1] range and convert to int16
const sample = Math.max(-1, Math.min(1, float32Data[i]));
int16Data[i] = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
}
// WAV file parameters
const channels = 1; // Mono
const bitsPerSample = 16;
const byteRate = (sampleRate * channels * bitsPerSample) / 8;
const blockAlign = (channels * bitsPerSample) / 8;
const dataSize = int16Data.length * 2;
const fileSize = 36 + dataSize;
// Create WAV header
const buffer = Buffer.alloc(44 + dataSize);
let offset = 0;
// RIFF chunk descriptor
buffer.write("RIFF", offset);
offset += 4;
buffer.writeUInt32LE(fileSize, offset);
offset += 4;
buffer.write("WAVE", offset);
offset += 4;
// fmt sub-chunk
buffer.write("fmt ", offset);
offset += 4;
buffer.writeUInt32LE(16, offset); // Subchunk1Size
offset += 4;
buffer.writeUInt16LE(1, offset); // AudioFormat (PCM)
offset += 2;
buffer.writeUInt16LE(channels, offset);
offset += 2;
buffer.writeUInt32LE(sampleRate, offset);
offset += 4;
buffer.writeUInt32LE(byteRate, offset);
offset += 4;
buffer.writeUInt16LE(blockAlign, offset);
offset += 2;
buffer.writeUInt16LE(bitsPerSample, offset);
offset += 2;
// data sub-chunk
buffer.write("data", offset);
offset += 4;
buffer.writeUInt32LE(dataSize, offset);
offset += 4;
// Write audio data
for (let i = 0; i < int16Data.length; i++) {
buffer.writeInt16LE(int16Data[i], offset);
offset += 2;
}
return buffer;
}

View file

@ -0,0 +1,124 @@
import { app } from "electron";
import * as fs from "node:fs";
import * as path from "node:path";
import { logger } from "../main/logger";
/**
* Clean up old audio files from the temporary directory
* @param maxAgeMs Maximum age of files to keep in milliseconds (default: 24 hours)
* @param maxSizeBytes Maximum total size of audio files in bytes (default: 500MB)
*/
export async function cleanupAudioFiles(options?: {
maxAgeMs?: number;
maxSizeBytes?: number;
}): Promise<void> {
const maxAgeMs = options?.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000; // 7 days
const maxSizeBytes = options?.maxSizeBytes ?? 500 * 1024 * 1024; // 500MB
const audioDir = path.join(app.getPath("temp"), "amical-audio");
try {
// Check if directory exists
if (!fs.existsSync(audioDir)) {
logger.main.debug("Audio directory does not exist, nothing to clean");
return;
}
const files = await fs.promises.readdir(audioDir);
const now = Date.now();
// Get file stats and sort by modified time (oldest first)
const fileStats = await Promise.all(
files.map(async (file) => {
const filePath = path.join(audioDir, file);
try {
const stats = await fs.promises.stat(filePath);
return {
path: filePath,
name: file,
size: stats.size,
mtime: stats.mtime.getTime(),
age: now - stats.mtime.getTime(),
};
} catch (error) {
logger.main.warn("Failed to stat audio file", { file, error });
return null;
}
}),
);
// Filter out null entries and audio files only
const audioFiles = fileStats.filter(
(stat) => stat !== null && stat.name.startsWith("audio-"),
) as NonNullable<(typeof fileStats)[number]>[];
// Sort by age (oldest first)
audioFiles.sort((a, b) => b.age - a.age);
let totalSize = 0;
let deletedCount = 0;
let deletedSize = 0;
for (const file of audioFiles) {
totalSize += file.size;
// Delete if file is too old or total size exceeds limit
if (file.age > maxAgeMs || totalSize > maxSizeBytes) {
try {
await fs.promises.unlink(file.path);
deletedCount++;
deletedSize += file.size;
logger.main.info("Deleted old audio file", {
file: file.name,
age: Math.round(file.age / 1000 / 60), // minutes
size: Math.round(file.size / 1024), // KB
});
} catch (error) {
logger.main.error("Failed to delete audio file", {
file: file.name,
error,
});
}
}
}
if (deletedCount > 0) {
logger.main.info("Audio cleanup completed", {
deletedCount,
deletedSizeMB: Math.round(deletedSize / 1024 / 1024),
remainingCount: audioFiles.length - deletedCount,
remainingSizeMB: Math.round((totalSize - deletedSize) / 1024 / 1024),
});
} else {
logger.main.debug("No audio files needed cleanup", {
totalCount: audioFiles.length,
totalSizeMB: Math.round(totalSize / 1024 / 1024),
});
}
} catch (error) {
logger.main.error("Audio cleanup failed", { error });
}
}
/**
* Delete a specific audio file
* @param filePath Path to the audio file to delete
*/
export async function deleteAudioFile(filePath: string): Promise<void> {
try {
// Ensure the file is in the audio directory
const audioDir = path.join(app.getPath("temp"), "amical-audio");
if (!filePath.startsWith(audioDir)) {
throw new Error("File is not in the audio directory");
}
await fs.promises.unlink(filePath);
logger.main.info("Deleted audio file", { filePath });
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
logger.main.error("Failed to delete audio file", { filePath, error });
throw error;
}
// File doesn't exist, that's fine
}
}