@hasna/recordings 0.1.15 → 0.1.16
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.16",
|
|
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.16";
|
|
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.16",
|
|
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.16";
|
|
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.16";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -16,7 +16,8 @@ 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
|
+
private var liveCommitTask: Task<Void, Never>?
|
|
20
21
|
private var isConfigured = false
|
|
21
22
|
private var pendingAudioChunks: [Data] = []
|
|
22
23
|
private var itemOrder: [String] = []
|
|
@@ -25,10 +26,14 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
25
26
|
private var committedItemIDs = Set<String>()
|
|
26
27
|
private var completedItemIDs = Set<String>()
|
|
27
28
|
private var completedEventCount = 0
|
|
29
|
+
private var queuedCommitCount = 0
|
|
28
30
|
private var uncommittedAudioBytes = 0
|
|
29
31
|
private var lastRealtimeEventAt = Date.distantPast
|
|
32
|
+
private var lastCommitQueuedAt = Date.distantPast
|
|
30
33
|
|
|
31
34
|
private nonisolated static let minimumManualCommitBytes = 5_760
|
|
35
|
+
private nonisolated static let minimumLiveCommitBytes = 38_400
|
|
36
|
+
private nonisolated static let liveCommitIntervalSeconds: TimeInterval = 0.8
|
|
32
37
|
|
|
33
38
|
private let apiKey: String
|
|
34
39
|
private let homePath: String
|
|
@@ -62,8 +67,10 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
62
67
|
committedItemIDs.removeAll(keepingCapacity: true)
|
|
63
68
|
completedItemIDs.removeAll(keepingCapacity: true)
|
|
64
69
|
completedEventCount = 0
|
|
70
|
+
queuedCommitCount = 0
|
|
65
71
|
uncommittedAudioBytes = 0
|
|
66
72
|
lastRealtimeEventAt = Date()
|
|
73
|
+
lastCommitQueuedAt = Date()
|
|
67
74
|
error = nil
|
|
68
75
|
|
|
69
76
|
var request = URLRequest(url: Self.transcriptionURL)
|
|
@@ -114,27 +121,29 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
114
121
|
"type": "input_audio_buffer.append",
|
|
115
122
|
"audio": base64,
|
|
116
123
|
]
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
await previousSendTask?.value
|
|
120
|
-
try? await self?.sendEvent(msg)
|
|
121
|
-
}
|
|
124
|
+
enqueueOutboundEvent(msg)
|
|
125
|
+
scheduleLiveCommitIfNeeded()
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
/// Signal end of input — triggers final transcription completion.
|
|
125
129
|
@discardableResult
|
|
126
|
-
public func commitInput() async -> Bool {
|
|
130
|
+
public func commitInput(reason: String = "final") async -> Bool {
|
|
127
131
|
guard isStreaming, ws != nil else { return false }
|
|
128
132
|
flushPendingAudio()
|
|
129
|
-
|
|
130
|
-
guard Self.shouldManuallyCommit(uncommittedAudioBytes:
|
|
133
|
+
let bytesToCommit = uncommittedAudioBytes
|
|
134
|
+
guard Self.shouldManuallyCommit(uncommittedAudioBytes: bytesToCommit) else {
|
|
131
135
|
return false
|
|
132
136
|
}
|
|
133
137
|
let msg: [String: Any] = [
|
|
134
138
|
"type": "input_audio_buffer.commit",
|
|
135
139
|
]
|
|
140
|
+
uncommittedAudioBytes = 0
|
|
141
|
+
queuedCommitCount += 1
|
|
136
142
|
lastRealtimeEventAt = Date()
|
|
137
|
-
|
|
143
|
+
lastCommitQueuedAt = Date()
|
|
144
|
+
NativeAppLog.write("realtime commit queued reason=\(reason) bytes=\(bytesToCommit)", homePath: homePath)
|
|
145
|
+
let commitTask = enqueueOutboundEvent(msg)
|
|
146
|
+
await commitTask?.value
|
|
138
147
|
return true
|
|
139
148
|
}
|
|
140
149
|
|
|
@@ -143,13 +152,15 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
143
152
|
guard isStreaming else { return accumulatedText }
|
|
144
153
|
let completedCountBeforeCommit = completedEventCount
|
|
145
154
|
let didManualCommit = await commitInput()
|
|
155
|
+
let expectedCommitCount = queuedCommitCount
|
|
146
156
|
|
|
147
157
|
let deadline = Date().addingTimeInterval(TimeInterval(timeoutMilliseconds) / 1_000)
|
|
148
158
|
while Date() < deadline, isStreaming {
|
|
149
159
|
let hasIncompleteCommittedItems = !committedItemIDs.subtracting(completedItemIDs).isEmpty
|
|
150
160
|
let hasManualCommitCompletion = !didManualCommit || completedEventCount > completedCountBeforeCommit
|
|
161
|
+
let hasQueuedCommitCompletion = completedEventCount >= expectedCommitCount
|
|
151
162
|
let quietLongEnough = Date().timeIntervalSince(lastRealtimeEventAt) >= 0.35
|
|
152
|
-
if !hasIncompleteCommittedItems, hasManualCommitCompletion, quietLongEnough {
|
|
163
|
+
if !hasIncompleteCommittedItems, hasManualCommitCompletion, hasQueuedCommitCompletion, quietLongEnough {
|
|
153
164
|
break
|
|
154
165
|
}
|
|
155
166
|
try? await Task.sleep(for: .milliseconds(50))
|
|
@@ -166,10 +177,12 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
166
177
|
isConfigured = false
|
|
167
178
|
receiveTask?.cancel()
|
|
168
179
|
ws?.cancel(with: .normalClosure, reason: nil)
|
|
169
|
-
|
|
180
|
+
outboundEventTask?.cancel()
|
|
181
|
+
liveCommitTask?.cancel()
|
|
170
182
|
ws = nil
|
|
171
183
|
receiveTask = nil
|
|
172
|
-
|
|
184
|
+
outboundEventTask = nil
|
|
185
|
+
liveCommitTask = nil
|
|
173
186
|
pendingAudioChunks.removeAll(keepingCapacity: true)
|
|
174
187
|
let text = accumulatedText
|
|
175
188
|
return text
|
|
@@ -214,7 +227,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
214
227
|
registerItem(itemID, previousItemID: json["previous_item_id"] as? String)
|
|
215
228
|
committedItemIDs.insert(itemID)
|
|
216
229
|
}
|
|
217
|
-
uncommittedAudioBytes = 0
|
|
218
230
|
|
|
219
231
|
case "conversation.item.input_audio_transcription.delta":
|
|
220
232
|
lastRealtimeEventAt = Date()
|
|
@@ -230,15 +242,19 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
230
242
|
lastRealtimeEventAt = Date()
|
|
231
243
|
let itemID = json["item_id"] as? String ?? "__default__"
|
|
232
244
|
registerItem(itemID, previousItemID: nil)
|
|
245
|
+
completedItemIDs.insert(itemID)
|
|
246
|
+
completedEventCount += 1
|
|
233
247
|
if let text = json["transcript"] as? String, !text.isEmpty {
|
|
234
248
|
completedTextByItem[itemID] = text
|
|
235
|
-
completedItemIDs.insert(itemID)
|
|
236
|
-
completedEventCount += 1
|
|
237
|
-
rebuildAccumulatedText()
|
|
238
249
|
}
|
|
250
|
+
rebuildAccumulatedText()
|
|
239
251
|
|
|
240
252
|
case "conversation.item.input_audio_transcription.failed":
|
|
241
253
|
lastRealtimeEventAt = Date()
|
|
254
|
+
if let itemID = json["item_id"] as? String {
|
|
255
|
+
completedItemIDs.insert(itemID)
|
|
256
|
+
completedEventCount += 1
|
|
257
|
+
}
|
|
242
258
|
if let msg = json["error"] as? [String: Any],
|
|
243
259
|
let message = msg["message"] as? String {
|
|
244
260
|
self.error = message
|
|
@@ -257,6 +273,37 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
257
273
|
}
|
|
258
274
|
}
|
|
259
275
|
|
|
276
|
+
@discardableResult
|
|
277
|
+
private func enqueueOutboundEvent(_ obj: [String: Any]) -> Task<Void, Never>? {
|
|
278
|
+
guard ws != nil else { return nil }
|
|
279
|
+
let previousTask = outboundEventTask
|
|
280
|
+
let task = Task { [weak self] in
|
|
281
|
+
await previousTask?.value
|
|
282
|
+
try? await self?.sendEvent(obj)
|
|
283
|
+
}
|
|
284
|
+
outboundEventTask = task
|
|
285
|
+
return task
|
|
286
|
+
}
|
|
287
|
+
|
|
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
|
+
|
|
260
307
|
private func registerItem(_ itemID: String, previousItemID: String?) {
|
|
261
308
|
guard !itemOrder.contains(itemID) else { return }
|
|
262
309
|
if let previousItemID, let previousIndex = itemOrder.firstIndex(of: previousItemID) {
|
|
@@ -336,6 +383,14 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
336
383
|
uncommittedAudioBytes >= minimumManualCommitBytes
|
|
337
384
|
}
|
|
338
385
|
|
|
386
|
+
private nonisolated static func shouldAutoCommitLiveInput(
|
|
387
|
+
uncommittedAudioBytes: Int,
|
|
388
|
+
secondsSinceLastCommit: TimeInterval
|
|
389
|
+
) -> Bool {
|
|
390
|
+
uncommittedAudioBytes >= minimumLiveCommitBytes &&
|
|
391
|
+
secondsSinceLastCommit >= liveCommitIntervalSeconds
|
|
392
|
+
}
|
|
393
|
+
|
|
339
394
|
private nonisolated static func transcriptionSessionUpdateEvent(transcription: [String: Any]) -> [String: Any] {
|
|
340
395
|
[
|
|
341
396
|
"type": "session.update",
|
|
@@ -348,12 +403,7 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
348
403
|
"rate": 24_000,
|
|
349
404
|
],
|
|
350
405
|
"transcription": transcription,
|
|
351
|
-
"turn_detection":
|
|
352
|
-
"type": "server_vad",
|
|
353
|
-
"threshold": 0.5,
|
|
354
|
-
"prefix_padding_ms": 300,
|
|
355
|
-
"silence_duration_ms": 350,
|
|
356
|
-
],
|
|
406
|
+
"turn_detection": NSNull(),
|
|
357
407
|
"noise_reduction": [
|
|
358
408
|
"type": "near_field",
|
|
359
409
|
],
|
|
@@ -408,6 +458,16 @@ extension RealtimeTranscriptionClient {
|
|
|
408
458
|
shouldManuallyCommit(uncommittedAudioBytes: uncommittedAudioBytes)
|
|
409
459
|
}
|
|
410
460
|
|
|
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
|
+
|
|
411
471
|
public nonisolated static func sessionUpdateTestHelper(prompt: String, language: String = "") -> [String: Any] {
|
|
412
472
|
var transcription: [String: Any] = [
|
|
413
473
|
"model": modelID,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Foundation
|
|
1
2
|
import Testing
|
|
2
3
|
@testable import RecordingsLib
|
|
3
4
|
|
|
@@ -98,8 +99,7 @@ struct RealtimeTranscriptionTests {
|
|
|
98
99
|
#expect(transcription?["prompt"] as? String == "Use Alumia as vocabulary")
|
|
99
100
|
#expect(transcription?["language"] as? String == "en")
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
#expect(turnDetection?["type"] as? String == "server_vad")
|
|
102
|
+
#expect(input?["turn_detection"] is NSNull)
|
|
103
103
|
|
|
104
104
|
let include = session?["include"] as? [String]
|
|
105
105
|
#expect(include?.contains("item.input_audio_transcription.logprobs") == true)
|
|
@@ -117,6 +117,22 @@ 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
|
+
|
|
120
136
|
@Test("Partial realtime text falls back for longer recordings")
|
|
121
137
|
func partialRealtimeFallback() {
|
|
122
138
|
#expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "Hi", pcmByteCount: 96_000) == true)
|