@hasna/recordings 0.1.13 → 0.1.15

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 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.13",
10
+ version: "0.1.15",
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.13";
10469
+ var VERSION = "0.1.15";
10470
10470
 
10471
10471
  // src/cli/index.ts
10472
10472
  var program = new Command;
@@ -10792,6 +10792,7 @@ appCommand.command("status").description("Show installed Recordings.app status")
10792
10792
  console.log(`Native sources: ${status.native_sources_available ? "available" : "missing"}`);
10793
10793
  console.log(`Installed app: ${status.installed ? status.installed_app_path : "missing"}`);
10794
10794
  console.log(`Executable: ${status.executable ? "available" : "missing"}`);
10795
+ console.log(`Code hash: ${status.app_code_hash ?? "unavailable"}`);
10795
10796
  if (process.platform === "darwin") {
10796
10797
  console.log(`Microphone: ${status.microphone_permission}`);
10797
10798
  console.log(`Accessibility: ${status.accessibility_permission}`);
@@ -10805,6 +10806,8 @@ appCommand.command("permissions").description("Show macOS permission state for R
10805
10806
  bundle_id: "com.hasna.recordings",
10806
10807
  microphone: status.microphone_permission,
10807
10808
  accessibility: status.accessibility_permission,
10809
+ app_code_hash: status.app_code_hash,
10810
+ ad_hoc_signed: status.ad_hoc_signed,
10808
10811
  log_path: status.log_path
10809
10812
  };
10810
10813
  if (program.opts().json) {
@@ -11316,6 +11319,8 @@ function getMacOSAppStatus() {
11316
11319
  const logPath = pathJoin(home, ".hasna", "recordings", "Recordings.log");
11317
11320
  const installerPath = pathJoin(packageRoot, "scripts", "install_macos_app.sh");
11318
11321
  const nativeSourcesPath = pathJoin(packageRoot, "src", "native", "Recordings");
11322
+ const signingInfo = getCodeSigningInfo(installedAppPath);
11323
+ const permissionCodeHash = signingInfo.adHoc ? signingInfo.cdHash : null;
11319
11324
  return {
11320
11325
  platform: process.platform,
11321
11326
  package_root: packageRoot,
@@ -11327,19 +11332,35 @@ function getMacOSAppStatus() {
11327
11332
  installed: existsSync6(installedAppPath),
11328
11333
  executable_path: executablePath,
11329
11334
  executable: existsSync6(executablePath),
11330
- microphone_permission: getTccPermission("kTCCServiceMicrophone", home),
11331
- accessibility_permission: getTccPermission("kTCCServiceAccessibility", home),
11335
+ app_code_hash: signingInfo.cdHash,
11336
+ ad_hoc_signed: signingInfo.adHoc,
11337
+ microphone_permission: getTccPermission("kTCCServiceMicrophone", home, permissionCodeHash),
11338
+ accessibility_permission: getTccPermission("kTCCServiceAccessibility", home, permissionCodeHash),
11332
11339
  log_path: logPath
11333
11340
  };
11334
11341
  }
11335
- function getTccPermission(service, home) {
11342
+ function getCodeSigningInfo(appPath) {
11343
+ if (process.platform !== "darwin" || !existsSync6(appPath)) {
11344
+ return { cdHash: null, adHoc: false };
11345
+ }
11346
+ const result = spawnSync("codesign", ["-d", "--verbose=4", appPath], {
11347
+ encoding: "utf8",
11348
+ stdio: ["ignore", "pipe", "pipe"]
11349
+ });
11350
+ const output = `${result.stdout}
11351
+ ${result.stderr}`;
11352
+ const cdHash = output.match(/^CDHash=([a-fA-F0-9]+)/m)?.[1]?.toLowerCase() ?? null;
11353
+ const adHoc = /Signature=adhoc/.test(output);
11354
+ return { cdHash, adHoc };
11355
+ }
11356
+ function getTccPermission(service, home, currentCodeHash) {
11336
11357
  if (process.platform !== "darwin")
11337
11358
  return "unsupported";
11338
11359
  const dbPaths = [
11339
11360
  pathJoin(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
11340
11361
  pathJoin("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
11341
11362
  ];
11342
- const sql = "select auth_value from access where service = '" + service.replace(/'/g, "''") + "' and client = 'com.hasna.recordings' order by last_modified desc limit 1;";
11363
+ const sql = "select auth_value || '|' || ifnull(hex(csreq), '') from access where service = '" + service.replace(/'/g, "''") + "' and client = 'com.hasna.recordings' order by last_modified desc limit 1;";
11343
11364
  for (const dbPath of dbPaths) {
11344
11365
  if (!existsSync6(dbPath))
11345
11366
  continue;
@@ -11348,8 +11369,14 @@ function getTccPermission(service, home) {
11348
11369
  stdio: ["ignore", "pipe", "ignore"]
11349
11370
  });
11350
11371
  const value = result.stdout.trim();
11351
- if (value)
11352
- return tccAuthValueLabel(value);
11372
+ if (!value)
11373
+ continue;
11374
+ const [authValue, csreqHex = ""] = value.split("|");
11375
+ const label = tccAuthValueLabel(authValue ?? "");
11376
+ if (label === "allowed" && currentCodeHash && csreqHex && !csreqHex.toLowerCase().includes(currentCodeHash.toLowerCase())) {
11377
+ return "stale_allowed_for_previous_app_build";
11378
+ }
11379
+ return label;
11353
11380
  }
11354
11381
  return "not_determined";
11355
11382
  }
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.13",
24
+ version: "0.1.15",
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.13";
14986
+ var VERSION = "0.1.15";
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.13";
1
+ export declare const VERSION = "0.1.15";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/recordings",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "description": "Speech-to-text recording tool with MCP and CLI — records, transcribes, and optionally enhances text using AI",
6
6
  "repository": {
@@ -73,4 +73,36 @@ rm -rf "$APP_DEST"
73
73
  mkdir -p "$DATA_DIR"
74
74
  cp -R "$APP_SOURCE" "$APP_DEST" || warn_or_fail "failed to copy app bundle"
75
75
 
76
+ current_cdhash() {
77
+ codesign -d --verbose=4 "$1" 2>&1 | awk -F= '/^CDHash=/ { print toupper($2); exit }'
78
+ }
79
+
80
+ tcc_csreq_hex() {
81
+ local db_path="$1"
82
+ local service="$2"
83
+ if [ ! -r "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
84
+ return 0
85
+ fi
86
+ sqlite3 "$db_path" \
87
+ "SELECT hex(csreq) FROM access WHERE service = '${service}' AND client = 'com.hasna.recordings' ORDER BY last_modified DESC LIMIT 1;" \
88
+ 2>/dev/null || true
89
+ }
90
+
91
+ reset_stale_permission() {
92
+ local service="$1"
93
+ local tcc_service="$2"
94
+ local db_path="$3"
95
+ local cdhash="$4"
96
+ local csreq_hex
97
+ csreq_hex="$(tcc_csreq_hex "$db_path" "$tcc_service" | tr '[:lower:]' '[:upper:]')"
98
+ if [ -n "$cdhash" ] && [ -n "$csreq_hex" ] && [[ "$csreq_hex" != *"$cdhash"* ]]; then
99
+ tccutil reset "$service" com.hasna.recordings >/dev/null 2>&1 || true
100
+ echo "Reset stale ${service} permission for the newly installed Recordings.app."
101
+ fi
102
+ }
103
+
104
+ APP_CDHASH="$(current_cdhash "$APP_DEST" || true)"
105
+ reset_stale_permission "Microphone" "kTCCServiceMicrophone" "${HOME}/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
106
+ reset_stale_permission "Accessibility" "kTCCServiceAccessibility" "/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
107
+
76
108
  echo "Installed Recordings.app from package: ${APP_DEST}"
@@ -1,5 +1,5 @@
1
1
  import SwiftUI
2
- import KeyboardShortcuts
2
+ @preconcurrency import KeyboardShortcuts
3
3
 
4
4
  public struct MenuBarPopover: View {
5
5
  @ObservedObject public var engine: RecordingEngine
@@ -16,12 +16,19 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
16
16
 
17
17
  private var ws: URLSessionWebSocketTask?
18
18
  private var receiveTask: Task<Void, Never>?
19
+ private var audioSendTask: Task<Void, Never>?
19
20
  private var isConfigured = false
20
21
  private var pendingAudioChunks: [Data] = []
21
22
  private var itemOrder: [String] = []
22
23
  private var deltaTextByItem: [String: String] = [:]
23
24
  private var completedTextByItem: [String: String] = [:]
25
+ private var committedItemIDs = Set<String>()
26
+ private var completedItemIDs = Set<String>()
24
27
  private var completedEventCount = 0
28
+ private var uncommittedAudioBytes = 0
29
+ private var lastRealtimeEventAt = Date.distantPast
30
+
31
+ private nonisolated static let minimumManualCommitBytes = 5_760
25
32
 
26
33
  private let apiKey: String
27
34
  private let homePath: String
@@ -52,7 +59,11 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
52
59
  itemOrder.removeAll(keepingCapacity: true)
53
60
  deltaTextByItem.removeAll(keepingCapacity: true)
54
61
  completedTextByItem.removeAll(keepingCapacity: true)
62
+ committedItemIDs.removeAll(keepingCapacity: true)
63
+ completedItemIDs.removeAll(keepingCapacity: true)
55
64
  completedEventCount = 0
65
+ uncommittedAudioBytes = 0
66
+ lastRealtimeEventAt = Date()
56
67
  error = nil
57
68
 
58
69
  var request = URLRequest(url: Self.transcriptionURL)
@@ -98,33 +109,49 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
98
109
  return
99
110
  }
100
111
  let base64 = data.base64EncodedString()
112
+ uncommittedAudioBytes += data.count
101
113
  let msg: [String: Any] = [
102
114
  "type": "input_audio_buffer.append",
103
115
  "audio": base64,
104
116
  ]
105
- Task { [weak self] in
117
+ let previousSendTask = audioSendTask
118
+ audioSendTask = Task { [weak self] in
119
+ await previousSendTask?.value
106
120
  try? await self?.sendEvent(msg)
107
121
  }
108
122
  }
109
123
 
110
124
  /// Signal end of input — triggers final transcription completion.
111
- public func commitInput() async {
112
- guard isStreaming, ws != nil else { return }
125
+ @discardableResult
126
+ public func commitInput() async -> Bool {
127
+ guard isStreaming, ws != nil else { return false }
113
128
  flushPendingAudio()
129
+ await audioSendTask?.value
130
+ guard Self.shouldManuallyCommit(uncommittedAudioBytes: uncommittedAudioBytes) else {
131
+ return false
132
+ }
114
133
  let msg: [String: Any] = [
115
134
  "type": "input_audio_buffer.commit",
116
135
  ]
136
+ lastRealtimeEventAt = Date()
117
137
  try? await sendEvent(msg)
138
+ return true
118
139
  }
119
140
 
120
141
  /// Commit buffered input, wait briefly for a final completed event, then close.
121
- public func finish(timeoutMilliseconds: UInt64 = 1_800) async -> String {
142
+ public func finish(timeoutMilliseconds: UInt64 = 2_800) async -> String {
122
143
  guard isStreaming else { return accumulatedText }
123
- let initialCompletedCount = completedEventCount
124
- await commitInput()
144
+ let completedCountBeforeCommit = completedEventCount
145
+ let didManualCommit = await commitInput()
125
146
 
126
147
  let deadline = Date().addingTimeInterval(TimeInterval(timeoutMilliseconds) / 1_000)
127
- while Date() < deadline, isStreaming, completedEventCount == initialCompletedCount {
148
+ while Date() < deadline, isStreaming {
149
+ let hasIncompleteCommittedItems = !committedItemIDs.subtracting(completedItemIDs).isEmpty
150
+ let hasManualCommitCompletion = !didManualCommit || completedEventCount > completedCountBeforeCommit
151
+ let quietLongEnough = Date().timeIntervalSince(lastRealtimeEventAt) >= 0.35
152
+ if !hasIncompleteCommittedItems, hasManualCommitCompletion, quietLongEnough {
153
+ break
154
+ }
128
155
  try? await Task.sleep(for: .milliseconds(50))
129
156
  }
130
157
 
@@ -139,8 +166,10 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
139
166
  isConfigured = false
140
167
  receiveTask?.cancel()
141
168
  ws?.cancel(with: .normalClosure, reason: nil)
169
+ audioSendTask?.cancel()
142
170
  ws = nil
143
171
  receiveTask = nil
172
+ audioSendTask = nil
144
173
  pendingAudioChunks.removeAll(keepingCapacity: true)
145
174
  let text = accumulatedText
146
175
  return text
@@ -180,11 +209,15 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
180
209
 
181
210
  switch type {
182
211
  case "input_audio_buffer.committed":
212
+ lastRealtimeEventAt = Date()
183
213
  if let itemID = json["item_id"] as? String {
184
214
  registerItem(itemID, previousItemID: json["previous_item_id"] as? String)
215
+ committedItemIDs.insert(itemID)
185
216
  }
217
+ uncommittedAudioBytes = 0
186
218
 
187
219
  case "conversation.item.input_audio_transcription.delta":
220
+ lastRealtimeEventAt = Date()
188
221
  let itemID = json["item_id"] as? String ?? "__default__"
189
222
  registerItem(itemID, previousItemID: nil)
190
223
  if let delta = json["delta"] as? String {
@@ -194,21 +227,25 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
194
227
 
195
228
  case "conversation.item.input_audio_transcription.completed":
196
229
  // The server may send the complete transcript here
230
+ lastRealtimeEventAt = Date()
197
231
  let itemID = json["item_id"] as? String ?? "__default__"
198
232
  registerItem(itemID, previousItemID: nil)
199
233
  if let text = json["transcript"] as? String, !text.isEmpty {
200
234
  completedTextByItem[itemID] = text
235
+ completedItemIDs.insert(itemID)
201
236
  completedEventCount += 1
202
237
  rebuildAccumulatedText()
203
238
  }
204
239
 
205
240
  case "conversation.item.input_audio_transcription.failed":
241
+ lastRealtimeEventAt = Date()
206
242
  if let msg = json["error"] as? [String: Any],
207
243
  let message = msg["message"] as? String {
208
244
  self.error = message
209
245
  }
210
246
 
211
247
  case "error":
248
+ lastRealtimeEventAt = Date()
212
249
  if let detail = json["error"] as? [String: Any],
213
250
  let msg = detail["message"] as? String {
214
251
  fputs("[RealtimeClient] Error: \(msg)\n", stderr)
@@ -295,6 +332,10 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
295
332
  return output
296
333
  }
297
334
 
335
+ private nonisolated static func shouldManuallyCommit(uncommittedAudioBytes: Int) -> Bool {
336
+ uncommittedAudioBytes >= minimumManualCommitBytes
337
+ }
338
+
298
339
  private nonisolated static func transcriptionSessionUpdateEvent(transcription: [String: Any]) -> [String: Any] {
299
340
  [
300
341
  "type": "session.update",
@@ -363,6 +404,10 @@ extension RealtimeTranscriptionClient {
363
404
  joinTranscriptParts(parts)
364
405
  }
365
406
 
407
+ public nonisolated static func shouldManuallyCommitTestHelper(uncommittedAudioBytes: Int) -> Bool {
408
+ shouldManuallyCommit(uncommittedAudioBytes: uncommittedAudioBytes)
409
+ }
410
+
366
411
  public nonisolated static func sessionUpdateTestHelper(prompt: String, language: String = "") -> [String: Any] {
367
412
  var transcription: [String: Any] = [
368
413
  "model": modelID,
@@ -127,6 +127,7 @@ public final class RecordingEngine: ObservableObject {
127
127
  private var streamingText = ""
128
128
  private var recordedPCM = Data()
129
129
  private var activeAudioPath: String?
130
+ private var lastAccessibilityPromptAt: Date?
130
131
 
131
132
  // fn key monitor (CGEventTap-based, swallows fn to prevent emoji picker)
132
133
  private let fnMonitor = FnKeyMonitor()
@@ -456,7 +457,7 @@ public final class RecordingEngine: ObservableObject {
456
457
 
457
458
  self.liveTranscriptionText = ""
458
459
 
459
- if let text {
460
+ if let text, !Self.shouldFallbackFromPartialRealtime(text: text, pcmByteCount: self.recordedPCM.count) {
460
461
  self.log("finish using realtime text chars=\(text.count)")
461
462
  self.isTranscribing = false
462
463
  self.finishWithText(
@@ -467,6 +468,9 @@ public final class RecordingEngine: ObservableObject {
467
468
  activeProjectName: activeProjectName
468
469
  )
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")
473
+ }
470
474
  self.log("falling back to CLI transcription audioPath=\(audioPath)")
471
475
  self.fallbackTranscribe(
472
476
  audioPath: audioPath,
@@ -485,6 +489,13 @@ public final class RecordingEngine: ObservableObject {
485
489
  }
486
490
  }
487
491
 
492
+ public nonisolated static func shouldFallbackFromPartialRealtime(text: String, pcmByteCount: Int) -> Bool {
493
+ let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
494
+ guard pcmByteCount >= 48_000, !trimmed.isEmpty else { return false }
495
+ let words = trimmed.split(whereSeparator: { $0.isWhitespace || $0.isNewline })
496
+ return trimmed.count < 12 || words.count <= 2
497
+ }
498
+
488
499
  private func finishWithText(_ text: String, curMode: RecordingMode, targetAppBundleIdentifier: String?, activeProjectId: String?, activeProjectName: String?) {
489
500
  log("finishWithText mode=\(curMode.rawValue) chars=\(text.count)")
490
501
  if curMode == .command {
@@ -638,7 +649,7 @@ public final class RecordingEngine: ObservableObject {
638
649
  // MARK: - Command Mode
639
650
 
640
651
  private func runCommandMode(instruction: String) {
641
- guard ensureAccessibilityPermission(prompt: true) else {
652
+ guard ensureAccessibilityPermission(prompt: shouldPromptAccessibility()) else {
642
653
  log("command mode blocked by accessibility permission")
643
654
  statusMessage = "Enable Accessibility permission for Recordings to rewrite selected text"
644
655
  return
@@ -704,9 +715,12 @@ public final class RecordingEngine: ObservableObject {
704
715
  pb.clearContents()
705
716
  pb.setString(text, forType: .string)
706
717
 
707
- guard ensureAccessibilityPermission(prompt: true) else {
718
+ let prompted = shouldPromptAccessibility()
719
+ guard ensureAccessibilityPermission(prompt: prompted) else {
708
720
  log("paste blocked by accessibility permission; copied to clipboard")
709
- self.statusMessage = "Copied — enable Accessibility permission for Recordings to paste"
721
+ self.statusMessage = prompted
722
+ ? "Copied — approve Accessibility for this Recordings app"
723
+ : "Copied — waiting for Accessibility approval"
710
724
  return
711
725
  }
712
726
 
@@ -744,14 +758,27 @@ public final class RecordingEngine: ObservableObject {
744
758
  }
745
759
 
746
760
  private func ensureAccessibilityPermission(prompt: Bool) -> Bool {
761
+ if AXIsProcessTrusted() {
762
+ return true
763
+ }
747
764
  if !prompt {
748
- return AXIsProcessTrusted()
765
+ return false
749
766
  }
750
767
  return AXIsProcessTrustedWithOptions(
751
768
  ["AXTrustedCheckOptionPrompt" as CFString: true] as CFDictionary
752
769
  )
753
770
  }
754
771
 
772
+ private func shouldPromptAccessibility() -> Bool {
773
+ let now = Date()
774
+ if let lastAccessibilityPromptAt,
775
+ now.timeIntervalSince(lastAccessibilityPromptAt) < 20 {
776
+ return false
777
+ }
778
+ lastAccessibilityPromptAt = now
779
+ return true
780
+ }
781
+
755
782
  private func log(_ message: String) {
756
783
  NativeAppLog.write(message, homePath: home)
757
784
  }
@@ -1,5 +1,5 @@
1
1
  import SwiftUI
2
- import KeyboardShortcuts
2
+ @preconcurrency import KeyboardShortcuts
3
3
 
4
4
  public struct SettingsView: View {
5
5
  @ObservedObject public var engine: RecordingEngine
@@ -45,6 +45,32 @@ public struct SettingsView: View {
45
45
  .foregroundStyle(.secondary)
46
46
  }
47
47
 
48
+ Section("Permissions") {
49
+ HStack {
50
+ Text("Microphone")
51
+ Spacer()
52
+ Text(engine.microphonePermissionLabel)
53
+ .foregroundStyle(.secondary)
54
+ }
55
+ Button("Request Microphone") {
56
+ engine.requestMicrophonePermission()
57
+ }
58
+ HStack {
59
+ Text("Accessibility")
60
+ Spacer()
61
+ Text(engine.accessibilityPermissionLabel)
62
+ .foregroundStyle(.secondary)
63
+ }
64
+ HStack {
65
+ Button("Request Accessibility") {
66
+ engine.requestAccessibilityPermission()
67
+ }
68
+ Button("Open Accessibility Settings") {
69
+ engine.openAccessibilitySettings()
70
+ }
71
+ }
72
+ }
73
+
48
74
  Section("System Prompt") {
49
75
  TextEditor(text: $projectStore.settings.globalSystemPrompt)
50
76
  .frame(height: 80)
@@ -110,4 +110,17 @@ struct RealtimeTranscriptionTests {
110
110
  let text = RealtimeTranscriptionClient.joinTranscriptPartsTestHelper(["Hello", "world.", " Next"])
111
111
  #expect(text == "Hello world. Next")
112
112
  }
113
+
114
+ @Test("Manual commit waits for enough buffered PCM audio")
115
+ func manualCommitThreshold() {
116
+ #expect(RealtimeTranscriptionClient.shouldManuallyCommitTestHelper(uncommittedAudioBytes: 4_799) == false)
117
+ #expect(RealtimeTranscriptionClient.shouldManuallyCommitTestHelper(uncommittedAudioBytes: 5_760) == true)
118
+ }
119
+
120
+ @Test("Partial realtime text falls back for longer recordings")
121
+ func partialRealtimeFallback() {
122
+ #expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "Hi", pcmByteCount: 96_000) == true)
123
+ #expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "This is a complete sentence.", pcmByteCount: 96_000) == false)
124
+ #expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "Hi", pcmByteCount: 12_000) == false)
125
+ }
113
126
  }