@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.
|
|
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.
|
|
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.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.
|
|
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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
guard Self.shouldManuallyCommit(uncommittedAudioBytes:
|
|
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
|
-
|
|
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
|
-
|
|
173
|
+
outboundEventTask?.cancel()
|
|
170
174
|
ws = nil
|
|
171
175
|
receiveTask = nil
|
|
172
|
-
|
|
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(
|
|
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
|
|
455
|
+
let realtimeText = streamingResult.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : streamingResult
|
|
457
456
|
|
|
458
457
|
self.liveTranscriptionText = ""
|
|
459
458
|
|
|
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")
|
|
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("
|
|
463
|
+
self.log("transcribing captured full audio audioPath=\(audioPath)")
|
|
475
464
|
self.fallbackTranscribe(
|
|
476
465
|
audioPath: audioPath,
|
|
477
466
|
curMode: curMode,
|