@hasna/recordings 0.1.25 → 0.1.26
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.
- package/dist/cli/index.js +13 -8
- package/dist/cli/options.d.ts +7 -0
- package/dist/cli/options.d.ts.map +1 -0
- package/dist/mcp/index.js +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +62 -6
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +48 -0
package/dist/cli/index.js
CHANGED
|
@@ -5098,7 +5098,7 @@ var init_pg_migrate = __esm(() => {
|
|
|
5098
5098
|
var require_package = __commonJS((exports, module) => {
|
|
5099
5099
|
module.exports = {
|
|
5100
5100
|
name: "@hasna/recordings",
|
|
5101
|
-
version: "0.1.
|
|
5101
|
+
version: "0.1.26",
|
|
5102
5102
|
type: "module",
|
|
5103
5103
|
description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
|
|
5104
5104
|
repository: {
|
|
@@ -6081,7 +6081,7 @@ async function processText(rawText, config, systemPrompt) {
|
|
|
6081
6081
|
}
|
|
6082
6082
|
|
|
6083
6083
|
// src/version.ts
|
|
6084
|
-
var VERSION = "0.1.
|
|
6084
|
+
var VERSION = "0.1.26";
|
|
6085
6085
|
|
|
6086
6086
|
// src/cli/storage.ts
|
|
6087
6087
|
import chalk from "chalk";
|
|
@@ -6442,6 +6442,14 @@ function registerStorageCommands(program) {
|
|
|
6442
6442
|
});
|
|
6443
6443
|
}
|
|
6444
6444
|
|
|
6445
|
+
// src/cli/options.ts
|
|
6446
|
+
function applyEnhancementOptions(config, opts) {
|
|
6447
|
+
if (opts.enhance === false || opts.noEnhance === false) {
|
|
6448
|
+
config.auto_enhance = false;
|
|
6449
|
+
}
|
|
6450
|
+
return config;
|
|
6451
|
+
}
|
|
6452
|
+
|
|
6445
6453
|
// src/cli/index.ts
|
|
6446
6454
|
var program = new Command;
|
|
6447
6455
|
program.name("recordings").description("Speech-to-text recording tool \u2014 record, transcribe, and enhance with AI").version(VERSION).option("--json", "Output as JSON").option("--agent <name>", "Agent name or ID").option("--project <name>", "Project name or ID").option("--session <id>", "Session ID");
|
|
@@ -6451,8 +6459,7 @@ program.command("record").description("Record from microphone, transcribe, and o
|
|
|
6451
6459
|
ensureDataDir(config);
|
|
6452
6460
|
if (opts.language)
|
|
6453
6461
|
config.language = opts.language;
|
|
6454
|
-
|
|
6455
|
-
config.auto_enhance = false;
|
|
6462
|
+
applyEnhancementOptions(config, opts);
|
|
6456
6463
|
const deps = await checkRecordingDeps();
|
|
6457
6464
|
if (!deps.available) {
|
|
6458
6465
|
console.error(chalk2.red(`Error: ${deps.message}`));
|
|
@@ -6518,8 +6525,7 @@ Saved as ${recording.id.slice(0, 8)}`));
|
|
|
6518
6525
|
program.command("transcribe <file>").description("Transcribe an existing audio file").option("--no-enhance", "Skip AI enhancement").option("--stream", "Stream transcription deltas while the file is processed").option("-t, --tags <tags>", "Comma-separated tags").option("--prompt <prompt>", "Vocabulary/context prompt for transcription").option("--system-prompt <prompt>", "System prompt for enhancement context").action(async (file, opts) => {
|
|
6519
6526
|
const config = loadConfig();
|
|
6520
6527
|
ensureDataDir(config);
|
|
6521
|
-
|
|
6522
|
-
config.auto_enhance = false;
|
|
6528
|
+
applyEnhancementOptions(config, opts);
|
|
6523
6529
|
const parentOpts = program.opts();
|
|
6524
6530
|
if (!parentOpts.json) {
|
|
6525
6531
|
console.log(chalk2.blue("Transcribing..."));
|
|
@@ -6883,8 +6889,7 @@ program.command("listen").description("Push-to-talk mode \u2014 press Space to s
|
|
|
6883
6889
|
ensureDataDir(config);
|
|
6884
6890
|
if (opts.language)
|
|
6885
6891
|
config.language = opts.language;
|
|
6886
|
-
|
|
6887
|
-
config.auto_enhance = false;
|
|
6892
|
+
applyEnhancementOptions(config, opts);
|
|
6888
6893
|
const deps = await checkRecordingDeps();
|
|
6889
6894
|
if (!deps.available) {
|
|
6890
6895
|
console.error(chalk2.red(`Error: ${deps.message}`));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RecordingsConfig } from "../types/index.js";
|
|
2
|
+
export type EnhancementOptionBag = {
|
|
3
|
+
enhance?: boolean;
|
|
4
|
+
noEnhance?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function applyEnhancementOptions(config: RecordingsConfig, opts: EnhancementOptionBag): RecordingsConfig;
|
|
7
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/cli/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,oBAAoB,GACzB,gBAAgB,CAKlB"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -4920,7 +4920,7 @@ var require_lib2 = __commonJS((exports, module) => {
|
|
|
4920
4920
|
var require_package = __commonJS((exports, module) => {
|
|
4921
4921
|
module.exports = {
|
|
4922
4922
|
name: "@hasna/recordings",
|
|
4923
|
-
version: "0.1.
|
|
4923
|
+
version: "0.1.26",
|
|
4924
4924
|
type: "module",
|
|
4925
4925
|
description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
|
|
4926
4926
|
repository: {
|
|
@@ -9797,7 +9797,7 @@ async function processText(rawText, config, systemPrompt) {
|
|
|
9797
9797
|
}
|
|
9798
9798
|
|
|
9799
9799
|
// src/version.ts
|
|
9800
|
-
var VERSION = "0.1.
|
|
9800
|
+
var VERSION = "0.1.26";
|
|
9801
9801
|
|
|
9802
9802
|
// src/db/storage-config.ts
|
|
9803
9803
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.26";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -44,7 +44,7 @@ public struct TranscriptionResult: Sendable {
|
|
|
44
44
|
var displayText: String { processedText ?? rawText }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
public enum RecordingTrigger {
|
|
47
|
+
public enum RecordingTrigger: Equatable, Sendable {
|
|
48
48
|
case manual
|
|
49
49
|
case fnKey
|
|
50
50
|
case keyboardShortcut
|
|
@@ -121,6 +121,7 @@ public final class RecordingEngine: ObservableObject {
|
|
|
121
121
|
private var recordingTimer: Timer?
|
|
122
122
|
private var activeTrigger: RecordingTrigger?
|
|
123
123
|
private var keyboardShortcutIsDown = false
|
|
124
|
+
private var fnKeyIsDown = false
|
|
124
125
|
private var targetAppBundleIdentifier: String?
|
|
125
126
|
private var targetAppPid: pid_t?
|
|
126
127
|
public var projectStore: ProjectStore?
|
|
@@ -165,13 +166,23 @@ public final class RecordingEngine: ObservableObject {
|
|
|
165
166
|
// Set up fn key monitor — hold fn to record, release to stop (like WisprFlow)
|
|
166
167
|
fnMonitor.onFnKeyDown = { [weak self] in
|
|
167
168
|
Task { @MainActor [weak self] in
|
|
168
|
-
guard let self
|
|
169
|
+
guard let self else { return }
|
|
170
|
+
self.fnKeyIsDown = true
|
|
171
|
+
guard self.useFnKey, Self.canBeginRecording(isRecording: self.isRecording, isTranscribing: self.isTranscribing) else { return }
|
|
169
172
|
self.startRecording(trigger: .fnKey)
|
|
170
173
|
}
|
|
171
174
|
}
|
|
172
175
|
fnMonitor.onFnKeyUp = { [weak self] in
|
|
173
176
|
Task { @MainActor [weak self] in
|
|
174
|
-
guard let self
|
|
177
|
+
guard let self else { return }
|
|
178
|
+
self.fnKeyIsDown = false
|
|
179
|
+
guard self.useFnKey, self.activeTrigger == .fnKey else { return }
|
|
180
|
+
guard self.isRecording else {
|
|
181
|
+
self.log("fn released before recording started; cancelling pending start")
|
|
182
|
+
self.resetRecordingIntent()
|
|
183
|
+
self.updateStatus()
|
|
184
|
+
return
|
|
185
|
+
}
|
|
175
186
|
self.stopAndTranscribe()
|
|
176
187
|
}
|
|
177
188
|
}
|
|
@@ -181,7 +192,7 @@ public final class RecordingEngine: ObservableObject {
|
|
|
181
192
|
Task { @MainActor [weak self] in
|
|
182
193
|
guard let self, !self.keyboardShortcutIsDown else { return }
|
|
183
194
|
self.keyboardShortcutIsDown = true
|
|
184
|
-
guard
|
|
195
|
+
guard Self.canBeginRecording(isRecording: self.isRecording, isTranscribing: self.isTranscribing) else { return }
|
|
185
196
|
self.startRecording(trigger: .keyboardShortcut)
|
|
186
197
|
}
|
|
187
198
|
}
|
|
@@ -189,7 +200,13 @@ public final class RecordingEngine: ObservableObject {
|
|
|
189
200
|
Task { @MainActor [weak self] in
|
|
190
201
|
guard let self, self.keyboardShortcutIsDown else { return }
|
|
191
202
|
self.keyboardShortcutIsDown = false
|
|
192
|
-
guard self.
|
|
203
|
+
guard self.activeTrigger == .keyboardShortcut else { return }
|
|
204
|
+
guard self.isRecording else {
|
|
205
|
+
self.log("shortcut released before recording started; cancelling pending start")
|
|
206
|
+
self.resetRecordingIntent()
|
|
207
|
+
self.updateStatus()
|
|
208
|
+
return
|
|
209
|
+
}
|
|
193
210
|
self.stopAndTranscribe()
|
|
194
211
|
}
|
|
195
212
|
}
|
|
@@ -289,7 +306,12 @@ public final class RecordingEngine: ObservableObject {
|
|
|
289
306
|
// MARK: - Start Recording (Streaming)
|
|
290
307
|
|
|
291
308
|
public func startRecording(trigger: RecordingTrigger = .manual) {
|
|
292
|
-
guard
|
|
309
|
+
guard Self.canBeginRecording(isRecording: isRecording, isTranscribing: isTranscribing) else {
|
|
310
|
+
if isTranscribing {
|
|
311
|
+
statusMessage = "Finish transcribing before recording again"
|
|
312
|
+
}
|
|
313
|
+
return
|
|
314
|
+
}
|
|
293
315
|
log("startRecording trigger=\(trigger) microphoneStatus=\(AVCaptureDevice.authorizationStatus(for: .audio).rawValue) accessibility=\(AXIsProcessTrusted())")
|
|
294
316
|
activeTrigger = trigger
|
|
295
317
|
keyboardShortcutIsDown = trigger == .keyboardShortcut
|
|
@@ -320,6 +342,12 @@ public final class RecordingEngine: ObservableObject {
|
|
|
320
342
|
guard let self else { return }
|
|
321
343
|
self.log("microphone access response granted=\(granted)")
|
|
322
344
|
if granted {
|
|
345
|
+
guard self.shouldContinueStarting(trigger: trigger) else {
|
|
346
|
+
self.log("recording start cancelled before microphone permission completed trigger=\(trigger)")
|
|
347
|
+
self.resetRecordingIntent()
|
|
348
|
+
self.updateStatus()
|
|
349
|
+
return
|
|
350
|
+
}
|
|
323
351
|
self.startNativeRecording()
|
|
324
352
|
} else {
|
|
325
353
|
self.resetRecordingIntent()
|
|
@@ -337,6 +365,33 @@ public final class RecordingEngine: ObservableObject {
|
|
|
337
365
|
}
|
|
338
366
|
}
|
|
339
367
|
|
|
368
|
+
nonisolated static func canBeginRecording(isRecording: Bool, isTranscribing: Bool) -> Bool {
|
|
369
|
+
!isRecording && !isTranscribing
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
nonisolated static func shouldContinueStartingAfterPermission(
|
|
373
|
+
trigger: RecordingTrigger,
|
|
374
|
+
keyboardShortcutIsDown: Bool,
|
|
375
|
+
fnKeyIsDown: Bool
|
|
376
|
+
) -> Bool {
|
|
377
|
+
switch trigger {
|
|
378
|
+
case .manual:
|
|
379
|
+
return true
|
|
380
|
+
case .keyboardShortcut:
|
|
381
|
+
return keyboardShortcutIsDown
|
|
382
|
+
case .fnKey:
|
|
383
|
+
return fnKeyIsDown
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private func shouldContinueStarting(trigger: RecordingTrigger) -> Bool {
|
|
388
|
+
activeTrigger == trigger && Self.shouldContinueStartingAfterPermission(
|
|
389
|
+
trigger: trigger,
|
|
390
|
+
keyboardShortcutIsDown: keyboardShortcutIsDown,
|
|
391
|
+
fnKeyIsDown: fnKeyIsDown
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
340
395
|
private func startNativeRecording() {
|
|
341
396
|
let streamState = PCMStreamState()
|
|
342
397
|
pcmStreamState = streamState
|
|
@@ -667,6 +722,7 @@ public final class RecordingEngine: ObservableObject {
|
|
|
667
722
|
private func resetRecordingIntent() {
|
|
668
723
|
activeTrigger = nil
|
|
669
724
|
keyboardShortcutIsDown = false
|
|
725
|
+
fnKeyIsDown = false
|
|
670
726
|
targetAppBundleIdentifier = nil
|
|
671
727
|
targetAppPid = nil
|
|
672
728
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
@testable import RecordingsLib
|
|
3
|
+
|
|
4
|
+
struct RecordingStartGateTests {
|
|
5
|
+
@Test("recording cannot begin while already recording or transcribing")
|
|
6
|
+
func cannotBeginWhenBusy() {
|
|
7
|
+
#expect(RecordingEngine.canBeginRecording(isRecording: false, isTranscribing: false) == true)
|
|
8
|
+
#expect(RecordingEngine.canBeginRecording(isRecording: true, isTranscribing: false) == false)
|
|
9
|
+
#expect(RecordingEngine.canBeginRecording(isRecording: false, isTranscribing: true) == false)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Test("manual recording can continue after permission grant")
|
|
13
|
+
func manualPermissionContinuation() {
|
|
14
|
+
#expect(RecordingEngine.shouldContinueStartingAfterPermission(
|
|
15
|
+
trigger: .manual,
|
|
16
|
+
keyboardShortcutIsDown: false,
|
|
17
|
+
fnKeyIsDown: false
|
|
18
|
+
))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Test("keyboard shortcut recording is cancelled when released before permission grant")
|
|
22
|
+
func keyboardPermissionContinuationRequiresHeldKey() {
|
|
23
|
+
#expect(RecordingEngine.shouldContinueStartingAfterPermission(
|
|
24
|
+
trigger: .keyboardShortcut,
|
|
25
|
+
keyboardShortcutIsDown: true,
|
|
26
|
+
fnKeyIsDown: false
|
|
27
|
+
))
|
|
28
|
+
#expect(RecordingEngine.shouldContinueStartingAfterPermission(
|
|
29
|
+
trigger: .keyboardShortcut,
|
|
30
|
+
keyboardShortcutIsDown: false,
|
|
31
|
+
fnKeyIsDown: false
|
|
32
|
+
) == false)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test("fn recording is cancelled when released before permission grant")
|
|
36
|
+
func fnPermissionContinuationRequiresHeldKey() {
|
|
37
|
+
#expect(RecordingEngine.shouldContinueStartingAfterPermission(
|
|
38
|
+
trigger: .fnKey,
|
|
39
|
+
keyboardShortcutIsDown: false,
|
|
40
|
+
fnKeyIsDown: true
|
|
41
|
+
))
|
|
42
|
+
#expect(RecordingEngine.shouldContinueStartingAfterPermission(
|
|
43
|
+
trigger: .fnKey,
|
|
44
|
+
keyboardShortcutIsDown: false,
|
|
45
|
+
fnKeyIsDown: false
|
|
46
|
+
) == false)
|
|
47
|
+
}
|
|
48
|
+
}
|