@hasna/recordings 0.1.16 → 0.1.18
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 +2 -2
- package/dist/mcp/index.js +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +1 -2
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +6 -47
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +56 -31
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +38 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +2 -18
package/dist/cli/index.js
CHANGED
|
@@ -7,7 +7,7 @@ var __require = import.meta.require;
|
|
|
7
7
|
var require_package = __commonJS((exports, module) => {
|
|
8
8
|
module.exports = {
|
|
9
9
|
name: "@hasna/recordings",
|
|
10
|
-
version: "0.1.
|
|
10
|
+
version: "0.1.18",
|
|
11
11
|
type: "module",
|
|
12
12
|
description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
|
|
13
13
|
repository: {
|
|
@@ -10466,7 +10466,7 @@ async function processText(rawText, config, systemPrompt) {
|
|
|
10466
10466
|
}
|
|
10467
10467
|
|
|
10468
10468
|
// src/version.ts
|
|
10469
|
-
var VERSION = "0.1.
|
|
10469
|
+
var VERSION = "0.1.17";
|
|
10470
10470
|
|
|
10471
10471
|
// src/cli/index.ts
|
|
10472
10472
|
var program = new Command;
|
package/dist/mcp/index.js
CHANGED
|
@@ -21,7 +21,7 @@ var __require = import.meta.require;
|
|
|
21
21
|
var require_package = __commonJS((exports, module) => {
|
|
22
22
|
module.exports = {
|
|
23
23
|
name: "@hasna/recordings",
|
|
24
|
-
version: "0.1.
|
|
24
|
+
version: "0.1.18",
|
|
25
25
|
type: "module",
|
|
26
26
|
description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
|
|
27
27
|
repository: {
|
|
@@ -14983,7 +14983,7 @@ async function processText(rawText, config, systemPrompt) {
|
|
|
14983
14983
|
}
|
|
14984
14984
|
|
|
14985
14985
|
// src/version.ts
|
|
14986
|
-
var VERSION = "0.1.
|
|
14986
|
+
var VERSION = "0.1.17";
|
|
14987
14987
|
|
|
14988
14988
|
// src/mcp/index.ts
|
|
14989
14989
|
var config = loadConfig();
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.17";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -237,11 +237,10 @@ public struct MenuBarPopover: View {
|
|
|
237
237
|
showProject: filterProjectId == nil,
|
|
238
238
|
isCopied: copiedIndex == i
|
|
239
239
|
) {
|
|
240
|
-
NSPasteboard.general.clearContents()
|
|
241
|
-
NSPasteboard.general.setString(item.displayText, forType: .string)
|
|
242
240
|
withAnimation(.easeInOut(duration: 0.15)) {
|
|
243
241
|
copiedIndex = i
|
|
244
242
|
}
|
|
243
|
+
engine.pasteIntoFrontApp(item.displayText)
|
|
245
244
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
|
|
246
245
|
withAnimation { if copiedIndex == i { copiedIndex = nil } }
|
|
247
246
|
}
|
|
@@ -17,7 +17,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
17
17
|
private var ws: URLSessionWebSocketTask?
|
|
18
18
|
private var receiveTask: Task<Void, Never>?
|
|
19
19
|
private var outboundEventTask: Task<Void, Never>?
|
|
20
|
-
private var liveCommitTask: Task<Void, Never>?
|
|
21
20
|
private var isConfigured = false
|
|
22
21
|
private var pendingAudioChunks: [Data] = []
|
|
23
22
|
private var itemOrder: [String] = []
|
|
@@ -29,11 +28,8 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
29
28
|
private var queuedCommitCount = 0
|
|
30
29
|
private var uncommittedAudioBytes = 0
|
|
31
30
|
private var lastRealtimeEventAt = Date.distantPast
|
|
32
|
-
private var lastCommitQueuedAt = Date.distantPast
|
|
33
31
|
|
|
34
32
|
private nonisolated static let minimumManualCommitBytes = 5_760
|
|
35
|
-
private nonisolated static let minimumLiveCommitBytes = 38_400
|
|
36
|
-
private nonisolated static let liveCommitIntervalSeconds: TimeInterval = 0.8
|
|
37
33
|
|
|
38
34
|
private let apiKey: String
|
|
39
35
|
private let homePath: String
|
|
@@ -70,7 +66,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
70
66
|
queuedCommitCount = 0
|
|
71
67
|
uncommittedAudioBytes = 0
|
|
72
68
|
lastRealtimeEventAt = Date()
|
|
73
|
-
lastCommitQueuedAt = Date()
|
|
74
69
|
error = nil
|
|
75
70
|
|
|
76
71
|
var request = URLRequest(url: Self.transcriptionURL)
|
|
@@ -122,7 +117,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
122
117
|
"audio": base64,
|
|
123
118
|
]
|
|
124
119
|
enqueueOutboundEvent(msg)
|
|
125
|
-
scheduleLiveCommitIfNeeded()
|
|
126
120
|
}
|
|
127
121
|
|
|
128
122
|
/// Signal end of input — triggers final transcription completion.
|
|
@@ -140,7 +134,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
140
134
|
uncommittedAudioBytes = 0
|
|
141
135
|
queuedCommitCount += 1
|
|
142
136
|
lastRealtimeEventAt = Date()
|
|
143
|
-
lastCommitQueuedAt = Date()
|
|
144
137
|
NativeAppLog.write("realtime commit queued reason=\(reason) bytes=\(bytesToCommit)", homePath: homePath)
|
|
145
138
|
let commitTask = enqueueOutboundEvent(msg)
|
|
146
139
|
await commitTask?.value
|
|
@@ -178,11 +171,9 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
178
171
|
receiveTask?.cancel()
|
|
179
172
|
ws?.cancel(with: .normalClosure, reason: nil)
|
|
180
173
|
outboundEventTask?.cancel()
|
|
181
|
-
liveCommitTask?.cancel()
|
|
182
174
|
ws = nil
|
|
183
175
|
receiveTask = nil
|
|
184
176
|
outboundEventTask = nil
|
|
185
|
-
liveCommitTask = nil
|
|
186
177
|
pendingAudioChunks.removeAll(keepingCapacity: true)
|
|
187
178
|
let text = accumulatedText
|
|
188
179
|
return text
|
|
@@ -285,25 +276,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
285
276
|
return task
|
|
286
277
|
}
|
|
287
278
|
|
|
288
|
-
private func scheduleLiveCommitIfNeeded(now: Date = Date()) {
|
|
289
|
-
guard liveCommitTask == nil else { return }
|
|
290
|
-
let secondsSinceLastCommit = now.timeIntervalSince(lastCommitQueuedAt)
|
|
291
|
-
guard Self.shouldAutoCommitLiveInput(
|
|
292
|
-
uncommittedAudioBytes: uncommittedAudioBytes,
|
|
293
|
-
secondsSinceLastCommit: secondsSinceLastCommit
|
|
294
|
-
) else { return }
|
|
295
|
-
|
|
296
|
-
liveCommitTask = Task { [weak self] in
|
|
297
|
-
_ = await self?.commitLiveInput()
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
@discardableResult
|
|
302
|
-
private func commitLiveInput() async -> Bool {
|
|
303
|
-
defer { liveCommitTask = nil }
|
|
304
|
-
return await commitInput(reason: "live")
|
|
305
|
-
}
|
|
306
|
-
|
|
307
279
|
private func registerItem(_ itemID: String, previousItemID: String?) {
|
|
308
280
|
guard !itemOrder.contains(itemID) else { return }
|
|
309
281
|
if let previousItemID, let previousIndex = itemOrder.firstIndex(of: previousItemID) {
|
|
@@ -383,14 +355,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
383
355
|
uncommittedAudioBytes >= minimumManualCommitBytes
|
|
384
356
|
}
|
|
385
357
|
|
|
386
|
-
private nonisolated static func shouldAutoCommitLiveInput(
|
|
387
|
-
uncommittedAudioBytes: Int,
|
|
388
|
-
secondsSinceLastCommit: TimeInterval
|
|
389
|
-
) -> Bool {
|
|
390
|
-
uncommittedAudioBytes >= minimumLiveCommitBytes &&
|
|
391
|
-
secondsSinceLastCommit >= liveCommitIntervalSeconds
|
|
392
|
-
}
|
|
393
|
-
|
|
394
358
|
private nonisolated static func transcriptionSessionUpdateEvent(transcription: [String: Any]) -> [String: Any] {
|
|
395
359
|
[
|
|
396
360
|
"type": "session.update",
|
|
@@ -403,7 +367,12 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
403
367
|
"rate": 24_000,
|
|
404
368
|
],
|
|
405
369
|
"transcription": transcription,
|
|
406
|
-
"turn_detection":
|
|
370
|
+
"turn_detection": [
|
|
371
|
+
"type": "server_vad",
|
|
372
|
+
"threshold": 0.5,
|
|
373
|
+
"prefix_padding_ms": 300,
|
|
374
|
+
"silence_duration_ms": 350,
|
|
375
|
+
],
|
|
407
376
|
"noise_reduction": [
|
|
408
377
|
"type": "near_field",
|
|
409
378
|
],
|
|
@@ -458,16 +427,6 @@ extension RealtimeTranscriptionClient {
|
|
|
458
427
|
shouldManuallyCommit(uncommittedAudioBytes: uncommittedAudioBytes)
|
|
459
428
|
}
|
|
460
429
|
|
|
461
|
-
public nonisolated static func shouldAutoCommitLiveInputTestHelper(
|
|
462
|
-
uncommittedAudioBytes: Int,
|
|
463
|
-
secondsSinceLastCommit: TimeInterval
|
|
464
|
-
) -> Bool {
|
|
465
|
-
shouldAutoCommitLiveInput(
|
|
466
|
-
uncommittedAudioBytes: uncommittedAudioBytes,
|
|
467
|
-
secondsSinceLastCommit: secondsSinceLastCommit
|
|
468
|
-
)
|
|
469
|
-
}
|
|
470
|
-
|
|
471
430
|
public nonisolated static func sessionUpdateTestHelper(prompt: String, language: String = "") -> [String: Any] {
|
|
472
431
|
var transcription: [String: Any] = [
|
|
473
432
|
"model": modelID,
|
|
@@ -50,6 +50,12 @@ public enum RecordingTrigger {
|
|
|
50
50
|
case keyboardShortcut
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
struct PasteTargetCandidate: Equatable, Sendable {
|
|
54
|
+
let pid: pid_t
|
|
55
|
+
let bundleIdentifier: String?
|
|
56
|
+
let isRegularApp: Bool
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
private actor PCMStreamState {
|
|
54
60
|
private var recordedPCM = Data()
|
|
55
61
|
private var pendingChunk = Data()
|
|
@@ -382,13 +388,12 @@ public final class RecordingEngine: ObservableObject {
|
|
|
382
388
|
// MARK: - Real-time Streaming
|
|
383
389
|
|
|
384
390
|
private func startRealtimeStreaming(apiKey: String) {
|
|
385
|
-
let systemPrompt = projectStore?.effectiveSystemPrompt ?? ""
|
|
386
391
|
let client = RealtimeTranscriptionClient(apiKey: apiKey, homePath: home)
|
|
387
392
|
realtimeClient = client
|
|
388
393
|
log("realtime streaming task starting")
|
|
389
394
|
|
|
390
395
|
streamingTask = Task {
|
|
391
|
-
await client.startStreaming(
|
|
396
|
+
await client.startStreaming()
|
|
392
397
|
self.log("realtime start completed streaming=\(client.isStreaming) error=\(client.error ?? "")")
|
|
393
398
|
|
|
394
399
|
// Receive deltas
|
|
@@ -430,6 +435,7 @@ public final class RecordingEngine: ObservableObject {
|
|
|
430
435
|
|
|
431
436
|
let curMode = mode
|
|
432
437
|
let targetAppBundleIdentifier = targetAppBundleIdentifier
|
|
438
|
+
let targetAppPid = targetAppPid
|
|
433
439
|
let activeProjectId = projectStore?.settings.activeProjectId
|
|
434
440
|
let activeProjectName = projectStore?.activeProject?.name
|
|
435
441
|
let audioPath = activeAudioPath
|
|
@@ -453,29 +459,20 @@ public final class RecordingEngine: ObservableObject {
|
|
|
453
459
|
self.streamingTask?.cancel()
|
|
454
460
|
self.streamingTask = nil
|
|
455
461
|
|
|
456
|
-
let
|
|
462
|
+
let realtimeText = streamingResult.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : streamingResult
|
|
457
463
|
|
|
458
464
|
self.liveTranscriptionText = ""
|
|
459
465
|
|
|
460
|
-
if let
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
self.finishWithText(
|
|
464
|
-
text,
|
|
465
|
-
curMode: curMode,
|
|
466
|
-
targetAppBundleIdentifier: targetAppBundleIdentifier,
|
|
467
|
-
activeProjectId: activeProjectId,
|
|
468
|
-
activeProjectName: activeProjectName
|
|
469
|
-
)
|
|
470
|
-
} else if let audioPath, self.writeCapturedWAV(to: audioPath) {
|
|
471
|
-
if let text {
|
|
472
|
-
self.log("realtime text looked partial chars=\(text.count); falling back to CLI")
|
|
466
|
+
if let audioPath, self.writeCapturedWAV(to: audioPath) {
|
|
467
|
+
if let realtimeText {
|
|
468
|
+
self.log("realtime preview ignored for final chars=\(realtimeText.count)")
|
|
473
469
|
}
|
|
474
|
-
self.log("
|
|
470
|
+
self.log("transcribing captured full audio audioPath=\(audioPath)")
|
|
475
471
|
self.fallbackTranscribe(
|
|
476
472
|
audioPath: audioPath,
|
|
477
473
|
curMode: curMode,
|
|
478
474
|
targetAppBundleIdentifier: targetAppBundleIdentifier,
|
|
475
|
+
targetAppPid: targetAppPid,
|
|
479
476
|
activeProjectId: activeProjectId,
|
|
480
477
|
activeProjectName: activeProjectName
|
|
481
478
|
)
|
|
@@ -496,7 +493,7 @@ public final class RecordingEngine: ObservableObject {
|
|
|
496
493
|
return trimmed.count < 12 || words.count <= 2
|
|
497
494
|
}
|
|
498
495
|
|
|
499
|
-
private func finishWithText(_ text: String, curMode: RecordingMode, targetAppBundleIdentifier: String?, activeProjectId: String?, activeProjectName: String?) {
|
|
496
|
+
private func finishWithText(_ text: String, curMode: RecordingMode, targetAppBundleIdentifier: String?, targetAppPid: pid_t?, activeProjectId: String?, activeProjectName: String?) {
|
|
500
497
|
log("finishWithText mode=\(curMode.rawValue) chars=\(text.count)")
|
|
501
498
|
if curMode == .command {
|
|
502
499
|
runCommandMode(instruction: text)
|
|
@@ -505,7 +502,7 @@ public final class RecordingEngine: ObservableObject {
|
|
|
505
502
|
|
|
506
503
|
let shortcutText = voiceShortcuts?.match(text)
|
|
507
504
|
let output = shortcutText ?? text
|
|
508
|
-
pasteIntoFrontApp(output, targetAppBundleIdentifier: targetAppBundleIdentifier)
|
|
505
|
+
pasteIntoFrontApp(output, targetAppBundleIdentifier: targetAppBundleIdentifier, targetAppPid: targetAppPid)
|
|
509
506
|
recentTranscriptions.insert(
|
|
510
507
|
TranscriptionResult(
|
|
511
508
|
rawText: text,
|
|
@@ -587,7 +584,7 @@ public final class RecordingEngine: ObservableObject {
|
|
|
587
584
|
|
|
588
585
|
// MARK: - Fallback Transcription
|
|
589
586
|
|
|
590
|
-
private func fallbackTranscribe(audioPath: String, curMode: RecordingMode, targetAppBundleIdentifier: String?, activeProjectId: String?, activeProjectName: String?) {
|
|
587
|
+
private func fallbackTranscribe(audioPath: String, curMode: RecordingMode, targetAppBundleIdentifier: String?, targetAppPid: pid_t?, activeProjectId: String?, activeProjectName: String?) {
|
|
591
588
|
let homePath = home
|
|
592
589
|
|
|
593
590
|
isTranscribing = true
|
|
@@ -619,6 +616,7 @@ public final class RecordingEngine: ObservableObject {
|
|
|
619
616
|
text,
|
|
620
617
|
curMode: curMode,
|
|
621
618
|
targetAppBundleIdentifier: targetAppBundleIdentifier,
|
|
619
|
+
targetAppPid: targetAppPid,
|
|
622
620
|
activeProjectId: activeProjectId,
|
|
623
621
|
activeProjectName: activeProjectName
|
|
624
622
|
)
|
|
@@ -709,8 +707,8 @@ public final class RecordingEngine: ObservableObject {
|
|
|
709
707
|
|
|
710
708
|
// MARK: - Paste
|
|
711
709
|
|
|
712
|
-
func pasteIntoFrontApp(_ text: String, targetAppBundleIdentifier: String? = nil) {
|
|
713
|
-
log("paste requested chars=\(text.count) target=\(targetAppBundleIdentifier ?? "nil") accessibility=\(AXIsProcessTrusted())")
|
|
710
|
+
func pasteIntoFrontApp(_ text: String, targetAppBundleIdentifier: String? = nil, targetAppPid: pid_t? = nil) {
|
|
711
|
+
log("paste requested chars=\(text.count) target=\(targetAppBundleIdentifier ?? "nil") pid=\(targetAppPid.map(String.init) ?? "nil") accessibility=\(AXIsProcessTrusted())")
|
|
714
712
|
let pb = NSPasteboard.general
|
|
715
713
|
pb.clearContents()
|
|
716
714
|
pb.setString(text, forType: .string)
|
|
@@ -726,13 +724,22 @@ public final class RecordingEngine: ObservableObject {
|
|
|
726
724
|
|
|
727
725
|
let myPID = ProcessInfo.processInfo.processIdentifier
|
|
728
726
|
let runningApps = NSWorkspace.shared.runningApplications
|
|
729
|
-
let
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
727
|
+
let candidates = runningApps.map {
|
|
728
|
+
PasteTargetCandidate(
|
|
729
|
+
pid: $0.processIdentifier,
|
|
730
|
+
bundleIdentifier: $0.bundleIdentifier,
|
|
731
|
+
isRegularApp: $0.activationPolicy == .regular
|
|
732
|
+
)
|
|
733
|
+
}
|
|
734
|
+
let selectedTarget = Self.selectPasteTarget(
|
|
735
|
+
candidates: candidates,
|
|
736
|
+
currentPid: myPID,
|
|
737
|
+
targetBundleIdentifier: targetAppBundleIdentifier,
|
|
738
|
+
targetPid: targetAppPid
|
|
739
|
+
)
|
|
740
|
+
let targetApp = selectedTarget.flatMap { selected in
|
|
741
|
+
runningApps.first { $0.processIdentifier == selected.pid }
|
|
742
|
+
}
|
|
736
743
|
|
|
737
744
|
guard let app = targetApp else {
|
|
738
745
|
log("paste target app not found")
|
|
@@ -740,8 +747,8 @@ public final class RecordingEngine: ObservableObject {
|
|
|
740
747
|
return
|
|
741
748
|
}
|
|
742
749
|
|
|
743
|
-
// Activate
|
|
744
|
-
app.activate()
|
|
750
|
+
// Activate the exact app that owned focus when recording started, then paste after focus settles.
|
|
751
|
+
app.activate(options: [.activateIgnoringOtherApps])
|
|
745
752
|
|
|
746
753
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
747
754
|
let src = CGEventSource(stateID: .hidSystemState)
|
|
@@ -757,6 +764,24 @@ public final class RecordingEngine: ObservableObject {
|
|
|
757
764
|
}
|
|
758
765
|
}
|
|
759
766
|
|
|
767
|
+
nonisolated static func selectPasteTarget(
|
|
768
|
+
candidates: [PasteTargetCandidate],
|
|
769
|
+
currentPid: pid_t,
|
|
770
|
+
targetBundleIdentifier: String?,
|
|
771
|
+
targetPid: pid_t?
|
|
772
|
+
) -> PasteTargetCandidate? {
|
|
773
|
+
candidates.first {
|
|
774
|
+
guard let targetPid else { return false }
|
|
775
|
+
return $0.pid == targetPid && $0.pid != currentPid
|
|
776
|
+
} ?? candidates.first {
|
|
777
|
+
guard $0.pid != currentPid, $0.isRegularApp else { return false }
|
|
778
|
+
guard let targetBundleIdentifier else { return false }
|
|
779
|
+
return $0.bundleIdentifier == targetBundleIdentifier
|
|
780
|
+
} ?? candidates.first {
|
|
781
|
+
$0.isRegularApp && $0.pid != currentPid
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
760
785
|
private func ensureAccessibilityPermission(prompt: Bool) -> Bool {
|
|
761
786
|
if AXIsProcessTrusted() {
|
|
762
787
|
return true
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
@testable import RecordingsLib
|
|
3
|
+
|
|
4
|
+
struct PasteTargetTests {
|
|
5
|
+
@Test("paste target prefers captured process id over bundle fallback")
|
|
6
|
+
func prefersCapturedPid() {
|
|
7
|
+
let candidates = [
|
|
8
|
+
PasteTargetCandidate(pid: 10, bundleIdentifier: "com.editor", isRegularApp: true),
|
|
9
|
+
PasteTargetCandidate(pid: 20, bundleIdentifier: "com.editor", isRegularApp: true),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
let selected = RecordingEngine.selectPasteTarget(
|
|
13
|
+
candidates: candidates,
|
|
14
|
+
currentPid: 99,
|
|
15
|
+
targetBundleIdentifier: "com.editor",
|
|
16
|
+
targetPid: 20
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
#expect(selected?.pid == 20)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test("paste target ignores current app when falling back")
|
|
23
|
+
func ignoresCurrentApp() {
|
|
24
|
+
let candidates = [
|
|
25
|
+
PasteTargetCandidate(pid: 99, bundleIdentifier: "com.hasna.recordings", isRegularApp: true),
|
|
26
|
+
PasteTargetCandidate(pid: 30, bundleIdentifier: "com.notes", isRegularApp: true),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
let selected = RecordingEngine.selectPasteTarget(
|
|
30
|
+
candidates: candidates,
|
|
31
|
+
currentPid: 99,
|
|
32
|
+
targetBundleIdentifier: nil,
|
|
33
|
+
targetPid: nil
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
#expect(selected?.pid == 30)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import Foundation
|
|
2
1
|
import Testing
|
|
3
2
|
@testable import RecordingsLib
|
|
4
3
|
|
|
@@ -99,7 +98,8 @@ struct RealtimeTranscriptionTests {
|
|
|
99
98
|
#expect(transcription?["prompt"] as? String == "Use Alumia as vocabulary")
|
|
100
99
|
#expect(transcription?["language"] as? String == "en")
|
|
101
100
|
|
|
102
|
-
|
|
101
|
+
let turnDetection = input?["turn_detection"] as? [String: Any]
|
|
102
|
+
#expect(turnDetection?["type"] as? String == "server_vad")
|
|
103
103
|
|
|
104
104
|
let include = session?["include"] as? [String]
|
|
105
105
|
#expect(include?.contains("item.input_audio_transcription.logprobs") == true)
|
|
@@ -117,22 +117,6 @@ struct RealtimeTranscriptionTests {
|
|
|
117
117
|
#expect(RealtimeTranscriptionClient.shouldManuallyCommitTestHelper(uncommittedAudioBytes: 5_760) == true)
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
@Test("Live commits wait for enough audio and elapsed time")
|
|
121
|
-
func liveCommitThreshold() {
|
|
122
|
-
#expect(RealtimeTranscriptionClient.shouldAutoCommitLiveInputTestHelper(
|
|
123
|
-
uncommittedAudioBytes: 38_399,
|
|
124
|
-
secondsSinceLastCommit: 1.0
|
|
125
|
-
) == false)
|
|
126
|
-
#expect(RealtimeTranscriptionClient.shouldAutoCommitLiveInputTestHelper(
|
|
127
|
-
uncommittedAudioBytes: 38_400,
|
|
128
|
-
secondsSinceLastCommit: 0.79
|
|
129
|
-
) == false)
|
|
130
|
-
#expect(RealtimeTranscriptionClient.shouldAutoCommitLiveInputTestHelper(
|
|
131
|
-
uncommittedAudioBytes: 38_400,
|
|
132
|
-
secondsSinceLastCommit: 0.8
|
|
133
|
-
) == true)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
120
|
@Test("Partial realtime text falls back for longer recordings")
|
|
137
121
|
func partialRealtimeFallback() {
|
|
138
122
|
#expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "Hi", pcmByteCount: 96_000) == true)
|