@hasna/recordings 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/index.js +395 -37
  3. package/dist/db/database.d.ts.map +1 -1
  4. package/dist/db/recordings.d.ts.map +1 -1
  5. package/dist/index.js +78 -11
  6. package/dist/lib/config.d.ts.map +1 -1
  7. package/dist/lib/enhancer.d.ts.map +1 -1
  8. package/dist/lib/recorder.d.ts.map +1 -1
  9. package/dist/lib/transcriber.d.ts +8 -2
  10. package/dist/lib/transcriber.d.ts.map +1 -1
  11. package/dist/mcp/index.js +147 -30
  12. package/dist/types/index.d.ts +3 -0
  13. package/dist/types/index.d.ts.map +1 -1
  14. package/dist/version.d.ts +2 -0
  15. package/dist/version.d.ts.map +1 -0
  16. package/package.json +20 -3
  17. package/scripts/install_macos_app.sh +76 -0
  18. package/src/native/Recordings/{Recordings → App}/RecordingsApp.swift +5 -1
  19. package/src/native/Recordings/Package.resolved +21 -3
  20. package/src/native/Recordings/Package.swift +16 -5
  21. package/src/native/Recordings/{Recordings → RecordingsLib}/Info.plist +6 -0
  22. package/src/native/Recordings/{Recordings → RecordingsLib}/MenuBarPopover.swift +55 -11
  23. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +36 -0
  24. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +164 -0
  25. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +113 -0
  26. package/src/native/Recordings/{Recordings → RecordingsLib}/ProjectStore.swift +11 -11
  27. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +383 -0
  28. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +835 -0
  29. package/src/native/Recordings/{Recordings → RecordingsLib}/SettingsView.swift +19 -5
  30. package/src/native/Recordings/{Recordings → RecordingsLib}/VoiceShortcuts.swift +12 -8
  31. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +63 -0
  32. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +23 -0
  33. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +33 -0
  34. package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +92 -0
  35. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +58 -0
  36. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +113 -0
  37. package/src/native/Recordings/build.sh +6 -6
  38. package/.takumi/settings.local.json +0 -7
  39. package/bun.lock +0 -250
  40. package/bunfig.toml +0 -2
  41. package/src/__tests__/agents.test.ts +0 -136
  42. package/src/__tests__/config.test.ts +0 -252
  43. package/src/__tests__/database.test.ts +0 -167
  44. package/src/__tests__/enhancer.test.ts +0 -639
  45. package/src/__tests__/preload.ts +0 -4
  46. package/src/__tests__/projects.test.ts +0 -109
  47. package/src/__tests__/recorder.test.ts +0 -278
  48. package/src/__tests__/recordings.test.ts +0 -353
  49. package/src/__tests__/transcriber.test.ts +0 -322
  50. package/src/__tests__/types.test.ts +0 -75
  51. package/src/cli/index.ts +0 -988
  52. package/src/db/agents.ts +0 -104
  53. package/src/db/database.ts +0 -163
  54. package/src/db/pg-migrations.ts +0 -82
  55. package/src/db/projects.ts +0 -71
  56. package/src/db/recordings.ts +0 -225
  57. package/src/index.ts +0 -81
  58. package/src/lib/config.ts +0 -223
  59. package/src/lib/enhancer.ts +0 -173
  60. package/src/lib/recorder.ts +0 -198
  61. package/src/lib/transcriber.ts +0 -105
  62. package/src/mcp/index.ts +0 -464
  63. package/src/native/Recordings/Recordings/RecordingEngine.swift +0 -455
  64. package/src/native/Recordings/test_fn.swift +0 -79
  65. package/src/native/Recordings/test_fn2.swift +0 -33
  66. package/src/types/index.ts +0 -144
  67. package/tsconfig.json +0 -21
  68. /package/src/native/Recordings/{Recordings → RecordingsLib}/FnKeyMonitor.swift +0 -0
  69. /package/src/native/Recordings/{Recordings → RecordingsLib}/Recordings.entitlements +0 -0
@@ -1,353 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
- import { tmpdir } from "os";
3
- import { join } from "path";
4
- import { mkdirSync, rmSync, existsSync } from "fs";
5
- import {
6
- getDatabase,
7
- closeDatabase,
8
- resetDatabase,
9
- } from "../db/database.js";
10
- import {
11
- createRecording,
12
- getRecording,
13
- listRecordings,
14
- deleteRecording,
15
- searchRecordings,
16
- getRecordingStats,
17
- } from "../db/recordings.js";
18
- import type { Recording } from "../types/index.js";
19
- import { type Database } from "bun:sqlite";
20
-
21
- let tempDir: string;
22
- let db: Database;
23
-
24
- beforeEach(() => {
25
- resetDatabase();
26
- tempDir = join(tmpdir(), `open-recordings-test-rec-${Date.now()}-${Math.random().toString(36).slice(2)}`);
27
- mkdirSync(tempDir, { recursive: true });
28
- const dbPath = join(tempDir, "test.db");
29
- db = getDatabase(dbPath);
30
- });
31
-
32
- afterEach(() => {
33
- closeDatabase();
34
- resetDatabase();
35
- if (existsSync(tempDir)) {
36
- rmSync(tempDir, { recursive: true, force: true });
37
- }
38
- });
39
-
40
- describe("createRecording", () => {
41
- test("creates a recording with minimal input", () => {
42
- const rec = createRecording({ raw_text: "hello world" }, db);
43
- expect(rec).toBeDefined();
44
- expect(rec.id).toBeDefined();
45
- expect(rec.raw_text).toBe("hello world");
46
- expect(rec.processing_mode).toBe("raw");
47
- expect(rec.model_used).toBe("gpt-4o-mini-transcribe");
48
- expect(rec.tags).toEqual([]);
49
- expect(rec.metadata).toEqual({});
50
- expect(rec.audio_path).toBeNull();
51
- expect(rec.processed_text).toBeNull();
52
- expect(rec.enhancement_model).toBeNull();
53
- expect(rec.language).toBeNull();
54
- expect(rec.agent_id).toBeNull();
55
- expect(rec.project_id).toBeNull();
56
- expect(rec.session_id).toBeNull();
57
- expect(rec.duration_ms).toBe(0);
58
- expect(rec.created_at).toBeDefined();
59
- });
60
-
61
- test("creates a recording with all fields", () => {
62
- const rec = createRecording(
63
- {
64
- audio_path: "/tmp/test.wav",
65
- raw_text: "raw dictation",
66
- processed_text: "polished text",
67
- processing_mode: "enhanced",
68
- model_used: "whisper-1",
69
- enhancement_model: "gpt-4o",
70
- duration_ms: 5000,
71
- language: "en",
72
- tags: ["meeting", "important"],
73
- session_id: "sess-1",
74
- metadata: { source: "cli" },
75
- },
76
- db
77
- );
78
-
79
- expect(rec.audio_path).toBe("/tmp/test.wav");
80
- expect(rec.raw_text).toBe("raw dictation");
81
- expect(rec.processed_text).toBe("polished text");
82
- expect(rec.processing_mode).toBe("enhanced");
83
- expect(rec.model_used).toBe("whisper-1");
84
- expect(rec.enhancement_model).toBe("gpt-4o");
85
- expect(rec.duration_ms).toBe(5000);
86
- expect(rec.language).toBe("en");
87
- expect(rec.tags).toEqual(["meeting", "important"]);
88
- expect(rec.session_id).toBe("sess-1");
89
- expect(rec.metadata).toEqual({ source: "cli" });
90
- });
91
-
92
- test("inserts tags into recording_tags table", () => {
93
- const rec = createRecording(
94
- { raw_text: "test", tags: ["alpha", "beta"] },
95
- db
96
- );
97
- const tags = db
98
- .query("SELECT tag FROM recording_tags WHERE recording_id = ? ORDER BY tag")
99
- .all(rec.id) as { tag: string }[];
100
- expect(tags.map((t) => t.tag)).toEqual(["alpha", "beta"]);
101
- });
102
-
103
- test("handles empty tags array", () => {
104
- const rec = createRecording({ raw_text: "test", tags: [] }, db);
105
- const tags = db
106
- .query("SELECT tag FROM recording_tags WHERE recording_id = ?")
107
- .all(rec.id) as { tag: string }[];
108
- expect(tags).toHaveLength(0);
109
- });
110
- });
111
-
112
- describe("getRecording", () => {
113
- test("retrieves a recording by full ID", () => {
114
- const created = createRecording({ raw_text: "find me" }, db);
115
- const found = getRecording(created.id, db);
116
- expect(found).toBeDefined();
117
- expect(found!.id).toBe(created.id);
118
- expect(found!.raw_text).toBe("find me");
119
- });
120
-
121
- test("retrieves a recording by partial ID prefix", () => {
122
- const created = createRecording({ raw_text: "partial find" }, db);
123
- const prefix = created.id.substring(0, 8);
124
- const found = getRecording(prefix, db);
125
- expect(found).toBeDefined();
126
- expect(found!.id).toBe(created.id);
127
- });
128
-
129
- test("returns null for non-existent ID", () => {
130
- const found = getRecording("nonexistent-id", db);
131
- expect(found).toBeNull();
132
- });
133
- });
134
-
135
- describe("listRecordings", () => {
136
- test("returns all recordings ordered by created_at DESC", () => {
137
- createRecording({ raw_text: "first" }, db);
138
- createRecording({ raw_text: "second" }, db);
139
- createRecording({ raw_text: "third" }, db);
140
-
141
- const list = listRecordings(undefined, db);
142
- expect(list).toHaveLength(3);
143
- // Most recent first
144
- expect(list[0]!.raw_text).toBe("third");
145
- expect(list[2]!.raw_text).toBe("first");
146
- });
147
-
148
- test("returns empty array when no recordings", () => {
149
- const list = listRecordings(undefined, db);
150
- expect(list).toEqual([]);
151
- });
152
-
153
- test("filters by processing_mode", () => {
154
- createRecording({ raw_text: "raw one" }, db);
155
- createRecording({ raw_text: "enhanced one", processing_mode: "enhanced" }, db);
156
-
157
- const raw = listRecordings({ processing_mode: "raw" }, db);
158
- expect(raw).toHaveLength(1);
159
- expect(raw[0]!.raw_text).toBe("raw one");
160
-
161
- const enhanced = listRecordings({ processing_mode: "enhanced" }, db);
162
- expect(enhanced).toHaveLength(1);
163
- expect(enhanced[0]!.raw_text).toBe("enhanced one");
164
- });
165
-
166
- test("filters by session_id", () => {
167
- createRecording({ raw_text: "sess1", session_id: "s1" }, db);
168
- createRecording({ raw_text: "sess2", session_id: "s2" }, db);
169
-
170
- const results = listRecordings({ session_id: "s1" }, db);
171
- expect(results).toHaveLength(1);
172
- expect(results[0]!.raw_text).toBe("sess1");
173
- });
174
-
175
- test("filters by tags", () => {
176
- createRecording({ raw_text: "tagged", tags: ["important", "meeting"] }, db);
177
- createRecording({ raw_text: "other", tags: ["casual"] }, db);
178
-
179
- const results = listRecordings({ tags: ["important"] }, db);
180
- expect(results).toHaveLength(1);
181
- expect(results[0]!.raw_text).toBe("tagged");
182
- });
183
-
184
- test("filters by multiple tags (AND logic)", () => {
185
- createRecording({ raw_text: "both", tags: ["a", "b"] }, db);
186
- createRecording({ raw_text: "only-a", tags: ["a"] }, db);
187
-
188
- const results = listRecordings({ tags: ["a", "b"] }, db);
189
- expect(results).toHaveLength(1);
190
- expect(results[0]!.raw_text).toBe("both");
191
- });
192
-
193
- test("filters by search text", () => {
194
- createRecording({ raw_text: "the quick brown fox" }, db);
195
- createRecording({ raw_text: "lazy dog" }, db);
196
-
197
- const results = listRecordings({ search: "brown fox" }, db);
198
- expect(results).toHaveLength(1);
199
- expect(results[0]!.raw_text).toBe("the quick brown fox");
200
- });
201
-
202
- test("search matches processed_text", () => {
203
- createRecording({ raw_text: "raw", processed_text: "polished golden text" }, db);
204
- createRecording({ raw_text: "other" }, db);
205
-
206
- const results = listRecordings({ search: "golden" }, db);
207
- expect(results).toHaveLength(1);
208
- expect(results[0]!.processed_text).toBe("polished golden text");
209
- });
210
-
211
- test("filters by since date", () => {
212
- const rec = createRecording({ raw_text: "recent" }, db);
213
- const results = listRecordings({ since: "2000-01-01" }, db);
214
- expect(results).toHaveLength(1);
215
-
216
- const noResults = listRecordings({ since: "2099-01-01" }, db);
217
- expect(noResults).toHaveLength(0);
218
- });
219
-
220
- test("filters by until date", () => {
221
- createRecording({ raw_text: "old" }, db);
222
- const results = listRecordings({ until: "2099-01-01" }, db);
223
- expect(results).toHaveLength(1);
224
-
225
- const noResults = listRecordings({ until: "2000-01-01" }, db);
226
- expect(noResults).toHaveLength(0);
227
- });
228
-
229
- test("respects limit", () => {
230
- for (let i = 0; i < 10; i++) {
231
- createRecording({ raw_text: `rec-${i}` }, db);
232
- }
233
- const results = listRecordings({ limit: 3 }, db);
234
- expect(results).toHaveLength(3);
235
- });
236
-
237
- test("respects offset", () => {
238
- for (let i = 0; i < 5; i++) {
239
- createRecording({ raw_text: `rec-${i}` }, db);
240
- }
241
- const results = listRecordings({ limit: 2, offset: 2 }, db);
242
- expect(results).toHaveLength(2);
243
- });
244
-
245
- test("filters by agent_id", () => {
246
- // Create an agent first
247
- db.query("INSERT INTO agents (id, name, created_at, last_seen_at) VALUES (?, ?, ?, ?)").run(
248
- "agent-1", "maximus", new Date().toISOString(), new Date().toISOString()
249
- );
250
- createRecording({ raw_text: "by agent", agent_id: "agent-1" }, db);
251
- createRecording({ raw_text: "no agent" }, db);
252
-
253
- const results = listRecordings({ agent_id: "agent-1" }, db);
254
- expect(results).toHaveLength(1);
255
- expect(results[0]!.raw_text).toBe("by agent");
256
- });
257
-
258
- test("filters by project_id", () => {
259
- db.query("INSERT INTO projects (id, name, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(
260
- "proj-1", "my-project", "/tmp/proj", new Date().toISOString(), new Date().toISOString()
261
- );
262
- createRecording({ raw_text: "in project", project_id: "proj-1" }, db);
263
- createRecording({ raw_text: "no project" }, db);
264
-
265
- const results = listRecordings({ project_id: "proj-1" }, db);
266
- expect(results).toHaveLength(1);
267
- expect(results[0]!.raw_text).toBe("in project");
268
- });
269
-
270
- test("default limit is 50", () => {
271
- for (let i = 0; i < 60; i++) {
272
- createRecording({ raw_text: `rec-${i}` }, db);
273
- }
274
- const results = listRecordings(undefined, db);
275
- expect(results).toHaveLength(50);
276
- });
277
- });
278
-
279
- describe("deleteRecording", () => {
280
- test("deletes an existing recording and returns true", () => {
281
- const rec = createRecording({ raw_text: "delete me" }, db);
282
- const result = deleteRecording(rec.id, db);
283
- expect(result).toBe(true);
284
- expect(getRecording(rec.id, db)).toBeNull();
285
- });
286
-
287
- test("returns false for non-existent recording", () => {
288
- const result = deleteRecording("nonexistent", db);
289
- expect(result).toBe(false);
290
- });
291
-
292
- test("cascades tag deletion", () => {
293
- const rec = createRecording({ raw_text: "tagged", tags: ["a", "b"] }, db);
294
- deleteRecording(rec.id, db);
295
- const tags = db
296
- .query("SELECT * FROM recording_tags WHERE recording_id = ?")
297
- .all(rec.id) as unknown[];
298
- expect(tags).toHaveLength(0);
299
- });
300
- });
301
-
302
- describe("searchRecordings", () => {
303
- test("delegates to listRecordings with search filter", () => {
304
- createRecording({ raw_text: "searchable content here" }, db);
305
- createRecording({ raw_text: "nothing special" }, db);
306
-
307
- const results = searchRecordings("searchable", undefined, db);
308
- expect(results).toHaveLength(1);
309
- expect(results[0]!.raw_text).toBe("searchable content here");
310
- });
311
-
312
- test("combines search with other filters", () => {
313
- createRecording({ raw_text: "meeting notes alpha", session_id: "s1" }, db);
314
- createRecording({ raw_text: "meeting notes beta", session_id: "s2" }, db);
315
-
316
- const results = searchRecordings("meeting", { session_id: "s1" }, db);
317
- expect(results).toHaveLength(1);
318
- expect(results[0]!.raw_text).toBe("meeting notes alpha");
319
- });
320
- });
321
-
322
- describe("getRecordingStats", () => {
323
- test("returns zero stats when empty", () => {
324
- const stats = getRecordingStats(db);
325
- expect(stats.total).toBe(0);
326
- expect(stats.raw).toBe(0);
327
- expect(stats.enhanced).toBe(0);
328
- expect(stats.total_duration_ms).toBe(0);
329
- expect(stats.by_model).toEqual({});
330
- });
331
-
332
- test("counts totals correctly", () => {
333
- createRecording({ raw_text: "one", duration_ms: 1000 }, db);
334
- createRecording({ raw_text: "two", duration_ms: 2000, processing_mode: "enhanced" }, db);
335
- createRecording({ raw_text: "three", duration_ms: 3000 }, db);
336
-
337
- const stats = getRecordingStats(db);
338
- expect(stats.total).toBe(3);
339
- expect(stats.raw).toBe(2);
340
- expect(stats.enhanced).toBe(1);
341
- expect(stats.total_duration_ms).toBe(6000);
342
- });
343
-
344
- test("groups by model correctly", () => {
345
- createRecording({ raw_text: "a", model_used: "whisper-1" }, db);
346
- createRecording({ raw_text: "b", model_used: "whisper-1" }, db);
347
- createRecording({ raw_text: "c", model_used: "gpt-4o-mini-transcribe" }, db);
348
-
349
- const stats = getRecordingStats(db);
350
- expect(stats.by_model["whisper-1"]).toBe(2);
351
- expect(stats.by_model["gpt-4o-mini-transcribe"]).toBe(1);
352
- });
353
- });
@@ -1,322 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
2
- import { tmpdir } from "os";
3
- import { join } from "path";
4
- import { mkdirSync, rmSync, existsSync, writeFileSync } from "fs";
5
- import { resetClient } from "../lib/transcriber.js";
6
- import { TranscriptionError } from "../types/index.js";
7
- import { DEFAULT_CONFIG } from "../lib/config.js";
8
- import type { RecordingsConfig } from "../types/index.js";
9
-
10
- const config: RecordingsConfig = {
11
- ...DEFAULT_CONFIG,
12
- openai_api_key: "sk-test-key",
13
- enhancement_api_key: "sk-test-key",
14
- };
15
-
16
- let tempDir: string;
17
- let tempAudioFile: string;
18
-
19
- beforeEach(() => {
20
- resetClient();
21
- tempDir = join(tmpdir(), `open-recordings-test-trans-${Date.now()}-${Math.random().toString(36).slice(2)}`);
22
- mkdirSync(tempDir, { recursive: true });
23
- // Create a fake audio file for tests that need to open a file
24
- tempAudioFile = join(tempDir, "test.wav");
25
- writeFileSync(tempAudioFile, Buffer.from("fake-audio-content"));
26
- });
27
-
28
- afterEach(() => {
29
- resetClient();
30
- if (existsSync(tempDir)) {
31
- rmSync(tempDir, { recursive: true, force: true });
32
- }
33
- });
34
-
35
- describe("transcribeAudio", () => {
36
- test("throws TranscriptionError when no API key", async () => {
37
- const noKeyConfig = { ...config, openai_api_key: "" };
38
- const { transcribeAudio } = await import("../lib/transcriber.js");
39
- resetClient();
40
-
41
- try {
42
- await transcribeAudio(tempAudioFile, noKeyConfig);
43
- expect(true).toBe(false);
44
- } catch (err) {
45
- expect(err).toBeInstanceOf(TranscriptionError);
46
- expect((err as Error).message).toContain("API key not configured");
47
- }
48
- });
49
-
50
- test("calls OpenAI transcription API and returns result", async () => {
51
- mock.module("openai", () => ({
52
- default: class MockOpenAI {
53
- audio = {
54
- transcriptions: {
55
- create: mock(() =>
56
- Promise.resolve({
57
- text: "Hello world transcribed",
58
- language: "en",
59
- })
60
- ),
61
- },
62
- };
63
- },
64
- }));
65
-
66
- resetClient();
67
- const { transcribeAudio } = await import("../lib/transcriber.js");
68
- resetClient();
69
-
70
- const result = await transcribeAudio(tempAudioFile, config);
71
- expect(result.text).toBe("Hello world transcribed");
72
- expect(result.model).toBe(config.transcription_model);
73
- expect(result.duration_ms).toBeGreaterThanOrEqual(0);
74
- expect(result.language).toBe("en");
75
-
76
- resetClient();
77
- });
78
-
79
- test("handles null language from API", async () => {
80
- mock.module("openai", () => ({
81
- default: class MockOpenAI {
82
- audio = {
83
- transcriptions: {
84
- create: mock(() =>
85
- Promise.resolve({
86
- text: "Transcribed text",
87
- })
88
- ),
89
- },
90
- };
91
- },
92
- }));
93
-
94
- resetClient();
95
- const { transcribeAudio } = await import("../lib/transcriber.js");
96
- resetClient();
97
-
98
- const result = await transcribeAudio(tempAudioFile, config);
99
- expect(result.text).toBe("Transcribed text");
100
- expect(result.language).toBeUndefined(); // property not present
101
-
102
- resetClient();
103
- });
104
-
105
- test("wraps API errors in TranscriptionError", async () => {
106
- mock.module("openai", () => ({
107
- default: class MockOpenAI {
108
- audio = {
109
- transcriptions: {
110
- create: mock(() => Promise.reject(new Error("API rate limit exceeded"))),
111
- },
112
- };
113
- },
114
- }));
115
-
116
- resetClient();
117
- const { transcribeAudio } = await import("../lib/transcriber.js");
118
- resetClient();
119
-
120
- try {
121
- await transcribeAudio(tempAudioFile, config);
122
- expect(true).toBe(false);
123
- } catch (err) {
124
- expect(err).toBeInstanceOf(TranscriptionError);
125
- expect((err as Error).message).toContain("Transcription failed");
126
- expect((err as Error).message).toContain("API rate limit exceeded");
127
- }
128
-
129
- resetClient();
130
- });
131
-
132
- test("wraps non-Error exceptions in TranscriptionError", async () => {
133
- mock.module("openai", () => ({
134
- default: class MockOpenAI {
135
- audio = {
136
- transcriptions: {
137
- create: mock(() => Promise.reject("string error")),
138
- },
139
- };
140
- },
141
- }));
142
-
143
- resetClient();
144
- const { transcribeAudio } = await import("../lib/transcriber.js");
145
- resetClient();
146
-
147
- try {
148
- await transcribeAudio(tempAudioFile, config);
149
- expect(true).toBe(false);
150
- } catch (err) {
151
- expect(err).toBeInstanceOf(TranscriptionError);
152
- expect((err as Error).message).toContain("Transcription failed");
153
- }
154
-
155
- resetClient();
156
- });
157
-
158
- test("uses config language when provided", async () => {
159
- let capturedOpts: any = null;
160
- mock.module("openai", () => ({
161
- default: class MockOpenAI {
162
- audio = {
163
- transcriptions: {
164
- create: mock((opts: any) => {
165
- capturedOpts = opts;
166
- return Promise.resolve({ text: "test", language: "fr" });
167
- }),
168
- },
169
- };
170
- },
171
- }));
172
-
173
- resetClient();
174
- const { transcribeAudio } = await import("../lib/transcriber.js");
175
- resetClient();
176
-
177
- const frConfig = { ...config, language: "fr" };
178
- await transcribeAudio(tempAudioFile, frConfig);
179
- expect(capturedOpts.language).toBe("fr");
180
- expect(capturedOpts.model).toBe(config.transcription_model);
181
- expect(capturedOpts.response_format).toBe("json");
182
-
183
- resetClient();
184
- });
185
-
186
- test("omits language when not in config", async () => {
187
- let capturedOpts: any = null;
188
- mock.module("openai", () => ({
189
- default: class MockOpenAI {
190
- audio = {
191
- transcriptions: {
192
- create: mock((opts: any) => {
193
- capturedOpts = opts;
194
- return Promise.resolve({ text: "test" });
195
- }),
196
- },
197
- };
198
- },
199
- }));
200
-
201
- resetClient();
202
- const { transcribeAudio } = await import("../lib/transcriber.js");
203
- resetClient();
204
-
205
- const noLangConfig = { ...config, language: "" };
206
- await transcribeAudio(tempAudioFile, noLangConfig);
207
- expect(capturedOpts.language).toBeUndefined();
208
-
209
- resetClient();
210
- });
211
- });
212
-
213
- describe("transcribeBuffer", () => {
214
- test("throws TranscriptionError when no API key", async () => {
215
- const noKeyConfig = { ...config, openai_api_key: "" };
216
- const { transcribeBuffer } = await import("../lib/transcriber.js");
217
- resetClient();
218
-
219
- try {
220
- await transcribeBuffer(Buffer.from("test"), "test.wav", noKeyConfig);
221
- expect(true).toBe(false);
222
- } catch (err) {
223
- expect(err).toBeInstanceOf(TranscriptionError);
224
- expect((err as Error).message).toContain("API key not configured");
225
- }
226
- });
227
-
228
- test("creates File and calls transcription API", async () => {
229
- mock.module("openai", () => ({
230
- default: class MockOpenAI {
231
- audio = {
232
- transcriptions: {
233
- create: mock(() =>
234
- Promise.resolve({
235
- text: "Buffer transcription result",
236
- language: "en",
237
- })
238
- ),
239
- },
240
- };
241
- },
242
- }));
243
-
244
- resetClient();
245
- const { transcribeBuffer } = await import("../lib/transcriber.js");
246
- resetClient();
247
-
248
- const result = await transcribeBuffer(
249
- Buffer.from("fake audio data"),
250
- "test.wav",
251
- config
252
- );
253
- expect(result.text).toBe("Buffer transcription result");
254
- expect(result.model).toBe(config.transcription_model);
255
- expect(result.duration_ms).toBeGreaterThanOrEqual(0);
256
-
257
- resetClient();
258
- });
259
-
260
- test("wraps API errors in TranscriptionError for buffer", async () => {
261
- mock.module("openai", () => ({
262
- default: class MockOpenAI {
263
- audio = {
264
- transcriptions: {
265
- create: mock(() => Promise.reject(new Error("Buffer API error"))),
266
- },
267
- };
268
- },
269
- }));
270
-
271
- resetClient();
272
- const { transcribeBuffer } = await import("../lib/transcriber.js");
273
- resetClient();
274
-
275
- try {
276
- await transcribeBuffer(Buffer.from("test"), "test.mp3", config);
277
- expect(true).toBe(false);
278
- } catch (err) {
279
- expect(err).toBeInstanceOf(TranscriptionError);
280
- expect((err as Error).message).toContain("Transcription failed");
281
- expect((err as Error).message).toContain("Buffer API error");
282
- }
283
-
284
- resetClient();
285
- });
286
-
287
- test("handles various file extensions for MIME type", async () => {
288
- let capturedFile: any = null;
289
- mock.module("openai", () => ({
290
- default: class MockOpenAI {
291
- audio = {
292
- transcriptions: {
293
- create: mock((opts: any) => {
294
- capturedFile = opts.file;
295
- return Promise.resolve({ text: "ok" });
296
- }),
297
- },
298
- };
299
- },
300
- }));
301
-
302
- resetClient();
303
- const { transcribeBuffer } = await import("../lib/transcriber.js");
304
-
305
- // Test different extensions - each creates a File with the correct MIME type
306
- const extensions = ["wav", "mp3", "m4a", "webm", "mp4", "mpeg", "mpga", "xyz"];
307
- for (const ext of extensions) {
308
- resetClient();
309
- await transcribeBuffer(Buffer.from("test"), `test.${ext}`, config);
310
- expect(capturedFile).toBeDefined();
311
- }
312
-
313
- resetClient();
314
- });
315
- });
316
-
317
- describe("resetClient", () => {
318
- test("clears the singleton client safely", () => {
319
- resetClient();
320
- resetClient(); // Double reset is fine
321
- });
322
- });