@hasna/recordings 0.1.15 → 0.1.17

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.15",
10
+ version: "0.1.17",
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.15";
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.15",
24
+ version: "0.1.17",
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.15";
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.15";
1
+ export declare const VERSION = "0.1.17";
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.15",
3
+ "version": "0.1.17",
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": {
@@ -16,7 +16,7 @@ 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
+ private var outboundEventTask: Task<Void, Never>?
20
20
  private var isConfigured = false
21
21
  private var pendingAudioChunks: [Data] = []
22
22
  private var itemOrder: [String] = []
@@ -25,6 +25,7 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
25
25
  private var committedItemIDs = Set<String>()
26
26
  private var completedItemIDs = Set<String>()
27
27
  private var completedEventCount = 0
28
+ private var queuedCommitCount = 0
28
29
  private var uncommittedAudioBytes = 0
29
30
  private var lastRealtimeEventAt = Date.distantPast
30
31
 
@@ -62,6 +63,7 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
62
63
  committedItemIDs.removeAll(keepingCapacity: true)
63
64
  completedItemIDs.removeAll(keepingCapacity: true)
64
65
  completedEventCount = 0
66
+ queuedCommitCount = 0
65
67
  uncommittedAudioBytes = 0
66
68
  lastRealtimeEventAt = Date()
67
69
  error = nil
@@ -114,27 +116,27 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
114
116
  "type": "input_audio_buffer.append",
115
117
  "audio": base64,
116
118
  ]
117
- let previousSendTask = audioSendTask
118
- audioSendTask = Task { [weak self] in
119
- await previousSendTask?.value
120
- try? await self?.sendEvent(msg)
121
- }
119
+ enqueueOutboundEvent(msg)
122
120
  }
123
121
 
124
122
  /// Signal end of input — triggers final transcription completion.
125
123
  @discardableResult
126
- public func commitInput() async -> Bool {
124
+ public func commitInput(reason: String = "final") async -> Bool {
127
125
  guard isStreaming, ws != nil else { return false }
128
126
  flushPendingAudio()
129
- await audioSendTask?.value
130
- guard Self.shouldManuallyCommit(uncommittedAudioBytes: uncommittedAudioBytes) else {
127
+ let bytesToCommit = uncommittedAudioBytes
128
+ guard Self.shouldManuallyCommit(uncommittedAudioBytes: bytesToCommit) else {
131
129
  return false
132
130
  }
133
131
  let msg: [String: Any] = [
134
132
  "type": "input_audio_buffer.commit",
135
133
  ]
134
+ uncommittedAudioBytes = 0
135
+ queuedCommitCount += 1
136
136
  lastRealtimeEventAt = Date()
137
- try? await sendEvent(msg)
137
+ NativeAppLog.write("realtime commit queued reason=\(reason) bytes=\(bytesToCommit)", homePath: homePath)
138
+ let commitTask = enqueueOutboundEvent(msg)
139
+ await commitTask?.value
138
140
  return true
139
141
  }
140
142
 
@@ -143,13 +145,15 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
143
145
  guard isStreaming else { return accumulatedText }
144
146
  let completedCountBeforeCommit = completedEventCount
145
147
  let didManualCommit = await commitInput()
148
+ let expectedCommitCount = queuedCommitCount
146
149
 
147
150
  let deadline = Date().addingTimeInterval(TimeInterval(timeoutMilliseconds) / 1_000)
148
151
  while Date() < deadline, isStreaming {
149
152
  let hasIncompleteCommittedItems = !committedItemIDs.subtracting(completedItemIDs).isEmpty
150
153
  let hasManualCommitCompletion = !didManualCommit || completedEventCount > completedCountBeforeCommit
154
+ let hasQueuedCommitCompletion = completedEventCount >= expectedCommitCount
151
155
  let quietLongEnough = Date().timeIntervalSince(lastRealtimeEventAt) >= 0.35
152
- if !hasIncompleteCommittedItems, hasManualCommitCompletion, quietLongEnough {
156
+ if !hasIncompleteCommittedItems, hasManualCommitCompletion, hasQueuedCommitCompletion, quietLongEnough {
153
157
  break
154
158
  }
155
159
  try? await Task.sleep(for: .milliseconds(50))
@@ -166,10 +170,10 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
166
170
  isConfigured = false
167
171
  receiveTask?.cancel()
168
172
  ws?.cancel(with: .normalClosure, reason: nil)
169
- audioSendTask?.cancel()
173
+ outboundEventTask?.cancel()
170
174
  ws = nil
171
175
  receiveTask = nil
172
- audioSendTask = nil
176
+ outboundEventTask = nil
173
177
  pendingAudioChunks.removeAll(keepingCapacity: true)
174
178
  let text = accumulatedText
175
179
  return text
@@ -214,7 +218,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
214
218
  registerItem(itemID, previousItemID: json["previous_item_id"] as? String)
215
219
  committedItemIDs.insert(itemID)
216
220
  }
217
- uncommittedAudioBytes = 0
218
221
 
219
222
  case "conversation.item.input_audio_transcription.delta":
220
223
  lastRealtimeEventAt = Date()
@@ -230,15 +233,19 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
230
233
  lastRealtimeEventAt = Date()
231
234
  let itemID = json["item_id"] as? String ?? "__default__"
232
235
  registerItem(itemID, previousItemID: nil)
236
+ completedItemIDs.insert(itemID)
237
+ completedEventCount += 1
233
238
  if let text = json["transcript"] as? String, !text.isEmpty {
234
239
  completedTextByItem[itemID] = text
235
- completedItemIDs.insert(itemID)
236
- completedEventCount += 1
237
- rebuildAccumulatedText()
238
240
  }
241
+ rebuildAccumulatedText()
239
242
 
240
243
  case "conversation.item.input_audio_transcription.failed":
241
244
  lastRealtimeEventAt = Date()
245
+ if let itemID = json["item_id"] as? String {
246
+ completedItemIDs.insert(itemID)
247
+ completedEventCount += 1
248
+ }
242
249
  if let msg = json["error"] as? [String: Any],
243
250
  let message = msg["message"] as? String {
244
251
  self.error = message
@@ -257,6 +264,18 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
257
264
  }
258
265
  }
259
266
 
267
+ @discardableResult
268
+ private func enqueueOutboundEvent(_ obj: [String: Any]) -> Task<Void, Never>? {
269
+ guard ws != nil else { return nil }
270
+ let previousTask = outboundEventTask
271
+ let task = Task { [weak self] in
272
+ await previousTask?.value
273
+ try? await self?.sendEvent(obj)
274
+ }
275
+ outboundEventTask = task
276
+ return task
277
+ }
278
+
260
279
  private func registerItem(_ itemID: String, previousItemID: String?) {
261
280
  guard !itemOrder.contains(itemID) else { return }
262
281
  if let previousItemID, let previousIndex = itemOrder.firstIndex(of: previousItemID) {
@@ -382,13 +382,12 @@ public final class RecordingEngine: ObservableObject {
382
382
  // MARK: - Real-time Streaming
383
383
 
384
384
  private func startRealtimeStreaming(apiKey: String) {
385
- let systemPrompt = projectStore?.effectiveSystemPrompt ?? ""
386
385
  let client = RealtimeTranscriptionClient(apiKey: apiKey, homePath: home)
387
386
  realtimeClient = client
388
387
  log("realtime streaming task starting")
389
388
 
390
389
  streamingTask = Task {
391
- await client.startStreaming(systemPrompt: systemPrompt)
390
+ await client.startStreaming()
392
391
  self.log("realtime start completed streaming=\(client.isStreaming) error=\(client.error ?? "")")
393
392
 
394
393
  // Receive deltas
@@ -453,25 +452,15 @@ public final class RecordingEngine: ObservableObject {
453
452
  self.streamingTask?.cancel()
454
453
  self.streamingTask = nil
455
454
 
456
- let text = streamingResult.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : streamingResult
455
+ let realtimeText = streamingResult.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : streamingResult
457
456
 
458
457
  self.liveTranscriptionText = ""
459
458
 
460
- if let text, !Self.shouldFallbackFromPartialRealtime(text: text, pcmByteCount: self.recordedPCM.count) {
461
- self.log("finish using realtime text chars=\(text.count)")
462
- self.isTranscribing = false
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")
459
+ if let audioPath, self.writeCapturedWAV(to: audioPath) {
460
+ if let realtimeText {
461
+ self.log("realtime preview ignored for final chars=\(realtimeText.count)")
473
462
  }
474
- self.log("falling back to CLI transcription audioPath=\(audioPath)")
463
+ self.log("transcribing captured full audio audioPath=\(audioPath)")
475
464
  self.fallbackTranscribe(
476
465
  audioPath: audioPath,
477
466
  curMode: curMode,