diff --git a/src/commands.ts b/src/commands.ts index c080c87..2304353 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -320,9 +320,13 @@ export function runRm(args: string[]) { const r = db.run(`DELETE FROM screen_states WHERE id = ?`, [id]); if (r.changes > 0) { console.log(`removed s${id}`); removed++; } } else { + const row = db.prepare(`SELECT audio_path FROM audio_transcripts WHERE id = ?`).get(id) as any; + if (!row) { console.error(`skip: a${id} not found`); continue; } + if (row.audio_path) { + try { unlinkSync(row.audio_path); } catch {} + } const r = db.run(`DELETE FROM audio_transcripts WHERE id = ?`, [id]); if (r.changes > 0) { console.log(`removed a${id}`); removed++; } - else console.error(`skip: a${id} not found`); } } if (removed === 0) process.exit(1); diff --git a/src/import-export.test.ts b/src/import-export.test.ts index 274354a..1be4783 100644 --- a/src/import-export.test.ts +++ b/src/import-export.test.ts @@ -10,6 +10,7 @@ process.env.TUNR_DB_PATH = join(tmpDir, "tunr.db"); const { db } = await import("./lib/db"); const { runExport } = await import("./export"); const { runImport } = await import("./import"); +const { runRm } = await import("./commands"); const seed = () => { db.run(`DELETE FROM screen_states`); @@ -129,4 +130,17 @@ describe("export → import round-trip", () => { ).run("2026-04-27T10:10:00.000Z", "git", "dev", "commit msg", null); expect(counts()).toEqual(before); }); + + test("rm deletes audio file referenced by audio transcript", () => { + seed(); + const audioPath = join(tmpDir, "audio.wav"); + writeFileSync(audioPath, "wav"); + const row = db.prepare(`SELECT id FROM audio_transcripts LIMIT 1`).get() as any; + db.prepare(`UPDATE audio_transcripts SET audio_path = ? WHERE id = ?`).run(audioPath, row.id); + + runRm([`a${row.id}`]); + + expect(existsSync(audioPath)).toBe(false); + expect(counts().audio).toBe(0); + }); }); diff --git a/src/lib/engine.ts b/src/lib/engine.ts index 8889776..56493ff 100644 --- a/src/lib/engine.ts +++ b/src/lib/engine.ts @@ -1,6 +1,6 @@ import { join, dirname } from "path"; import { homedir } from "os"; -import { unlinkSync } from "fs"; +import { readdirSync, statSync, unlinkSync } from "fs"; import type { DenyRule } from "./types"; import { @@ -43,6 +43,9 @@ export function startEngine(log: Logger = defaultLog): EngineHandle { const settings = loadSettings(); let active = true; + cleanupAudioChunks(AUDIO_DIR, log); + cleanupAudioChunks(MIC_DIR, log); + // Reload settings.json on changes (single source of truth for runtime config) const reloadSettings = async () => { try { @@ -196,7 +199,7 @@ export function startEngine(log: Logger = defaultLog): EngineHandle { const chunk = JSON.parse(line); if (!chunk || typeof chunk.file !== "string" || typeof chunk.timestamp !== "string") continue; const wp = Bun.spawnSync(["whisper-cli", "-m", modelPath, "-l", "auto", "-f", chunk.file, "-np", "-nt"], { stdout: "pipe", stderr: "pipe" }); - try { unlinkSync(chunk.file); } catch {} + deleteAudioChunk(chunk.file, log); if (wp.exitCode !== 0) continue; const transcript = wp.stdout.toString().trim(); if (transcript) { @@ -234,3 +237,27 @@ function defaultLog(level: "info" | "warn" | "error", msg: string, extra?: Recor if (level === "error") console.error(line); else console.log(line); } + +function deleteAudioChunk(file: string, log: Logger) { + try { + unlinkSync(file); + } catch (err: any) { + if (err?.code !== "ENOENT") log("warn", "audio cleanup failed", { file, error: String(err) }); + } +} + +function cleanupAudioChunks(dir: string, log: Logger) { + try { + for (const name of readdirSync(dir)) { + if (name === ".keep") continue; + const file = join(dir, name); + try { + if (statSync(file).isFile()) deleteAudioChunk(file, log); + } catch (err: any) { + if (err?.code !== "ENOENT") log("warn", "audio cleanup failed", { file, error: String(err) }); + } + } + } catch (err: any) { + if (err?.code !== "ENOENT") log("warn", "audio cleanup failed", { dir, error: String(err) }); + } +}