@hasna/recordings 0.1.29 → 0.1.30
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/RecordingEngine.swift +97 -23
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +15 -5
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +12 -3
package/dist/cli/index.js
CHANGED
|
@@ -5098,7 +5098,7 @@ var init_pg_migrate = __esm(() => {
|
|
|
5098
5098
|
var require_package = __commonJS((exports, module) => {
|
|
5099
5099
|
module.exports = {
|
|
5100
5100
|
name: "@hasna/recordings",
|
|
5101
|
-
version: "0.1.
|
|
5101
|
+
version: "0.1.30",
|
|
5102
5102
|
type: "module",
|
|
5103
5103
|
description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
|
|
5104
5104
|
repository: {
|
|
@@ -6081,7 +6081,7 @@ async function processText(rawText, config, systemPrompt) {
|
|
|
6081
6081
|
}
|
|
6082
6082
|
|
|
6083
6083
|
// src/version.ts
|
|
6084
|
-
var VERSION = "0.1.
|
|
6084
|
+
var VERSION = "0.1.30";
|
|
6085
6085
|
|
|
6086
6086
|
// src/cli/storage.ts
|
|
6087
6087
|
import chalk from "chalk";
|
package/dist/mcp/index.js
CHANGED
|
@@ -4920,7 +4920,7 @@ var require_lib2 = __commonJS((exports, module) => {
|
|
|
4920
4920
|
var require_package = __commonJS((exports, module) => {
|
|
4921
4921
|
module.exports = {
|
|
4922
4922
|
name: "@hasna/recordings",
|
|
4923
|
-
version: "0.1.
|
|
4923
|
+
version: "0.1.30",
|
|
4924
4924
|
type: "module",
|
|
4925
4925
|
description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
|
|
4926
4926
|
repository: {
|
|
@@ -9797,7 +9797,7 @@ async function processText(rawText, config, systemPrompt) {
|
|
|
9797
9797
|
}
|
|
9798
9798
|
|
|
9799
9799
|
// src/version.ts
|
|
9800
|
-
var VERSION = "0.1.
|
|
9800
|
+
var VERSION = "0.1.30";
|
|
9801
9801
|
|
|
9802
9802
|
// src/db/storage-config.ts
|
|
9803
9803
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.30";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -558,20 +558,21 @@ public final class RecordingEngine: ObservableObject {
|
|
|
558
558
|
self.streamingTask = nil
|
|
559
559
|
|
|
560
560
|
let realtimeText = Self.normalizedRealtimeTranscript(streamingResult)
|
|
561
|
-
|
|
562
|
-
self.liveTranscriptionText = ""
|
|
563
|
-
|
|
564
|
-
if Self.shouldUseRealtimeFastPath(
|
|
561
|
+
let fastPathText = Self.realtimeFastPathTranscript(
|
|
565
562
|
realtimeText: streamingResult,
|
|
566
563
|
pcmByteCount: self.recordedPCM.count,
|
|
567
564
|
language: transcriptionLanguage
|
|
568
|
-
)
|
|
569
|
-
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
self.liveTranscriptionText = ""
|
|
568
|
+
|
|
569
|
+
if let fastPathText {
|
|
570
570
|
let releaseToTextMS = Int(Date().timeIntervalSince(stopStartedAt) * 1_000)
|
|
571
|
-
|
|
571
|
+
let repaired = Self.wasRealtimeTranscriptRepaired(rawText: streamingResult, cleanedText: fastPathText)
|
|
572
|
+
self.log("using realtime fast path chars=\(fastPathText.count) repaired=\(repaired) releaseToTextMs=\(releaseToTextMS)")
|
|
572
573
|
self.isTranscribing = false
|
|
573
574
|
self.finishWithText(
|
|
574
|
-
|
|
575
|
+
fastPathText,
|
|
575
576
|
curMode: curMode,
|
|
576
577
|
targetAppBundleIdentifier: targetAppBundleIdentifier,
|
|
577
578
|
targetAppPid: targetAppPid,
|
|
@@ -639,11 +640,23 @@ public final class RecordingEngine: ObservableObject {
|
|
|
639
640
|
pcmByteCount: Int,
|
|
640
641
|
language: String = "en"
|
|
641
642
|
) -> Bool {
|
|
642
|
-
|
|
643
|
+
realtimeFastPathTranscript(
|
|
644
|
+
realtimeText: realtimeText,
|
|
645
|
+
pcmByteCount: pcmByteCount,
|
|
646
|
+
language: language
|
|
647
|
+
) != nil
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
public nonisolated static func realtimeFastPathTranscript(
|
|
651
|
+
realtimeText: String?,
|
|
652
|
+
pcmByteCount: Int,
|
|
653
|
+
language: String = "en"
|
|
654
|
+
) -> String? {
|
|
655
|
+
guard let text = normalizedRealtimeTranscript(realtimeText) else { return nil }
|
|
643
656
|
guard isSafeRealtimeFastPathText(rawText: realtimeText ?? "", cleanedText: text, language: language) else {
|
|
644
|
-
return
|
|
657
|
+
return nil
|
|
645
658
|
}
|
|
646
|
-
return
|
|
659
|
+
return shouldFallbackFromPartialRealtime(text: text, pcmByteCount: pcmByteCount) ? nil : text
|
|
647
660
|
}
|
|
648
661
|
|
|
649
662
|
public nonisolated static func isSafeRealtimeFastPathText(rawText: String, cleanedText: String, language: String) -> Bool {
|
|
@@ -652,19 +665,31 @@ public final class RecordingEngine: ObservableObject {
|
|
|
652
665
|
guard languageHint == "en" else { return true }
|
|
653
666
|
|
|
654
667
|
let rawTrimmed = rawText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
655
|
-
|
|
668
|
+
let rawNormalized = normalizedTranscriptText(rawTrimmed)
|
|
669
|
+
let cleanedNormalized = normalizedTranscriptText(cleanedText)
|
|
670
|
+
guard !cleanedNormalized.isEmpty else { return false }
|
|
671
|
+
guard rawNormalized != cleanedNormalized else { return true }
|
|
672
|
+
guard cjkLetterCount(in: cleanedNormalized) == 0 else { return false }
|
|
656
673
|
|
|
657
|
-
let
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
)
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
674
|
+
let cleanedWords = canonicalTranscriptWords(cleanedNormalized, droppingArtifacts: false)
|
|
675
|
+
guard !cleanedWords.isEmpty else { return false }
|
|
676
|
+
|
|
677
|
+
let rawWords = canonicalTranscriptWords(rawNormalized, droppingArtifacts: true)
|
|
678
|
+
guard isSubsequence(cleanedWords, of: rawWords) else { return false }
|
|
679
|
+
|
|
680
|
+
let rawCJKCount = cjkLetterCount(in: rawNormalized)
|
|
681
|
+
if rawCJKCount > 0 {
|
|
682
|
+
let cleanedLatinCount = latinLetterCount(in: cleanedNormalized)
|
|
683
|
+
guard cleanedLatinCount >= max(2, rawCJKCount * 2) else { return false }
|
|
684
|
+
guard rawCJKCount <= max(6, cleanedLatinCount / 3) else { return false }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
let removedWordCount = max(0, rawWords.count - cleanedWords.count)
|
|
688
|
+
return removedWordCount <= max(8, cleanedWords.count + 3)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
public nonisolated static func wasRealtimeTranscriptRepaired(rawText: String, cleanedText: String) -> Bool {
|
|
692
|
+
normalizedTranscriptText(rawText) != normalizedTranscriptText(cleanedText)
|
|
668
693
|
}
|
|
669
694
|
|
|
670
695
|
public nonisolated static func cleanRealtimeArtifactText(_ text: String) -> String {
|
|
@@ -709,6 +734,55 @@ public final class RecordingEngine: ObservableObject {
|
|
|
709
734
|
return words.joined(separator: " ")
|
|
710
735
|
}
|
|
711
736
|
|
|
737
|
+
private nonisolated static func normalizedTranscriptText(_ text: String) -> String {
|
|
738
|
+
text
|
|
739
|
+
.replacingOccurrences(of: "\n", with: " ")
|
|
740
|
+
.replacingOccurrences(of: "\t", with: " ")
|
|
741
|
+
.replacingOccurrences(
|
|
742
|
+
of: #"\s+"#,
|
|
743
|
+
with: " ",
|
|
744
|
+
options: .regularExpression
|
|
745
|
+
)
|
|
746
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private nonisolated static func canonicalTranscriptWords(_ text: String, droppingArtifacts: Bool) -> [String] {
|
|
750
|
+
text.split(whereSeparator: { $0.isWhitespace || $0.isNewline }).compactMap { rawWord in
|
|
751
|
+
var normalized = String(rawWord)
|
|
752
|
+
.trimmingCharacters(in: .punctuationCharacters.union(.whitespacesAndNewlines))
|
|
753
|
+
.lowercased()
|
|
754
|
+
if droppingArtifacts {
|
|
755
|
+
normalized = normalized.unicodeScalars.reduce(into: "") { output, scalar in
|
|
756
|
+
if !isCJKScalar(scalar) {
|
|
757
|
+
output.unicodeScalars.append(scalar)
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
guard !normalized.isEmpty else { return nil }
|
|
762
|
+
if droppingArtifacts, isStandaloneRealtimeArtifact(normalized) {
|
|
763
|
+
return nil
|
|
764
|
+
}
|
|
765
|
+
return normalized
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private nonisolated static func isSubsequence(_ needle: [String], of haystack: [String]) -> Bool {
|
|
770
|
+
guard !needle.isEmpty else { return true }
|
|
771
|
+
var index = 0
|
|
772
|
+
for word in haystack where word == needle[index] {
|
|
773
|
+
index += 1
|
|
774
|
+
if index == needle.count {
|
|
775
|
+
return true
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return false
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private nonisolated static func isStandaloneRealtimeArtifact(_ word: String) -> Bool {
|
|
782
|
+
let artifactTokens: Set<String> = ["어", "음", "um", "umm", "uh", "uhh", "erm", "hmm", "eh"]
|
|
783
|
+
return artifactTokens.contains(word) || isMostlyCJKArtifact(word)
|
|
784
|
+
}
|
|
785
|
+
|
|
712
786
|
private nonisolated static func collapseAdjacentDuplicateWords(in text: String) -> String {
|
|
713
787
|
let words = text.split(separator: " ").map(String.init)
|
|
714
788
|
guard words.count > 1 else { return text }
|
|
@@ -126,22 +126,32 @@ struct RealtimeTranscriptionTests {
|
|
|
126
126
|
#expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "Hi", pcmByteCount: 12_000) == false)
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
@Test("Realtime fast path accepts useful
|
|
129
|
+
@Test("Realtime fast path accepts useful and safely repaired text")
|
|
130
130
|
func realtimeFastPathDecision() {
|
|
131
131
|
#expect(RecordingEngine.shouldUseRealtimeFastPath(realtimeText: " this is a useful transcript ", pcmByteCount: 96_000))
|
|
132
132
|
#expect(RecordingEngine.shouldUseRealtimeFastPath(realtimeText: "Hi", pcmByteCount: 12_000))
|
|
133
133
|
#expect(RecordingEngine.shouldUseRealtimeFastPath(realtimeText: "Hi", pcmByteCount: 96_000) == false)
|
|
134
134
|
#expect(RecordingEngine.shouldUseRealtimeFastPath(realtimeText: " ", pcmByteCount: 96_000) == false)
|
|
135
|
-
#expect(RecordingEngine.
|
|
135
|
+
#expect(RecordingEngine.realtimeFastPathTranscript(
|
|
136
136
|
realtimeText: "Actually 리수 Zoom your goal",
|
|
137
137
|
pcmByteCount: 96_000,
|
|
138
138
|
language: "en"
|
|
139
|
-
) ==
|
|
140
|
-
#expect(RecordingEngine.
|
|
139
|
+
) == "Actually Zoom your goal")
|
|
140
|
+
#expect(RecordingEngine.realtimeFastPathTranscript(
|
|
141
|
+
realtimeText: "어 Okay I don't know if this This is working어 Okay I don't know if this This is working",
|
|
142
|
+
pcmByteCount: 96_000,
|
|
143
|
+
language: "en"
|
|
144
|
+
) == "Okay I don't know if this is working")
|
|
145
|
+
#expect(RecordingEngine.realtimeFastPathTranscript(
|
|
141
146
|
realtimeText: "Actually Zoom your goal",
|
|
142
147
|
pcmByteCount: 96_000,
|
|
143
148
|
language: "en"
|
|
144
|
-
))
|
|
149
|
+
) == "Actually Zoom your goal")
|
|
150
|
+
#expect(RecordingEngine.realtimeFastPathTranscript(
|
|
151
|
+
realtimeText: "리수 度扫 開けたの 어",
|
|
152
|
+
pcmByteCount: 96_000,
|
|
153
|
+
language: "en"
|
|
154
|
+
) == nil)
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
@Test("Realtime artifact cleanup removes duplicated chunks and filler tokens")
|
|
@@ -64,17 +64,26 @@ struct TranscriptResolutionTests {
|
|
|
64
64
|
#expect(RecordingEngine.normalizedRealtimeTranscript(nil) == nil)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
@Test("
|
|
68
|
-
func
|
|
67
|
+
@Test("English realtime text can be safely repaired before fast-path paste")
|
|
68
|
+
func realtimeTextRepairedForFastPath() {
|
|
69
69
|
#expect(RecordingEngine.isSafeRealtimeFastPathText(
|
|
70
70
|
rawText: "Actually 리수 Zoom your goal",
|
|
71
71
|
cleanedText: "Actually Zoom your goal",
|
|
72
72
|
language: "en"
|
|
73
|
-
)
|
|
73
|
+
))
|
|
74
74
|
#expect(RecordingEngine.isSafeRealtimeFastPathText(
|
|
75
75
|
rawText: "Actually Zoom your goal",
|
|
76
76
|
cleanedText: "Actually Zoom your goal",
|
|
77
77
|
language: "en"
|
|
78
78
|
))
|
|
79
|
+
#expect(RecordingEngine.isSafeRealtimeFastPathText(
|
|
80
|
+
rawText: "Actually Zoom your goal",
|
|
81
|
+
cleanedText: "Actually invented Zoom your goal",
|
|
82
|
+
language: "en"
|
|
83
|
+
) == false)
|
|
84
|
+
#expect(RecordingEngine.wasRealtimeTranscriptRepaired(
|
|
85
|
+
rawText: "working어",
|
|
86
|
+
cleanedText: "working"
|
|
87
|
+
))
|
|
79
88
|
}
|
|
80
89
|
}
|