@hasna/recordings 0.1.11 → 0.1.13
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/README.md +2 -0
- package/dist/cli/index.js +395 -37
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/index.js +78 -11
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/transcriber.d.ts +8 -2
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.js +147 -30
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +20 -3
- package/scripts/install_macos_app.sh +76 -0
- package/src/native/Recordings/{Recordings → App}/RecordingsApp.swift +5 -1
- package/src/native/Recordings/Package.resolved +21 -3
- package/src/native/Recordings/Package.swift +16 -5
- package/src/native/Recordings/{Recordings → RecordingsLib}/Info.plist +6 -0
- package/src/native/Recordings/{Recordings → RecordingsLib}/MenuBarPopover.swift +55 -11
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +36 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +164 -0
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +113 -0
- package/src/native/Recordings/{Recordings → RecordingsLib}/ProjectStore.swift +11 -11
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +383 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +835 -0
- package/src/native/Recordings/{Recordings → RecordingsLib}/SettingsView.swift +19 -5
- package/src/native/Recordings/{Recordings → RecordingsLib}/VoiceShortcuts.swift +12 -8
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +63 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +23 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +33 -0
- package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +92 -0
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +58 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +113 -0
- package/src/native/Recordings/build.sh +6 -6
- package/.takumi/settings.local.json +0 -7
- package/bun.lock +0 -250
- package/bunfig.toml +0 -2
- package/src/__tests__/agents.test.ts +0 -136
- package/src/__tests__/config.test.ts +0 -252
- package/src/__tests__/database.test.ts +0 -167
- package/src/__tests__/enhancer.test.ts +0 -639
- package/src/__tests__/preload.ts +0 -4
- package/src/__tests__/projects.test.ts +0 -109
- package/src/__tests__/recorder.test.ts +0 -278
- package/src/__tests__/recordings.test.ts +0 -353
- package/src/__tests__/transcriber.test.ts +0 -322
- package/src/__tests__/types.test.ts +0 -75
- package/src/cli/index.ts +0 -988
- package/src/db/agents.ts +0 -104
- package/src/db/database.ts +0 -163
- package/src/db/pg-migrations.ts +0 -82
- package/src/db/projects.ts +0 -71
- package/src/db/recordings.ts +0 -225
- package/src/index.ts +0 -81
- package/src/lib/config.ts +0 -223
- package/src/lib/enhancer.ts +0 -173
- package/src/lib/recorder.ts +0 -198
- package/src/lib/transcriber.ts +0 -105
- package/src/mcp/index.ts +0 -464
- package/src/native/Recordings/Recordings/RecordingEngine.swift +0 -455
- package/src/native/Recordings/test_fn.swift +0 -79
- package/src/native/Recordings/test_fn2.swift +0 -33
- package/src/types/index.ts +0 -144
- package/tsconfig.json +0 -21
- /package/src/native/Recordings/{Recordings → RecordingsLib}/FnKeyMonitor.swift +0 -0
- /package/src/native/Recordings/{Recordings → RecordingsLib}/Recordings.entitlements +0 -0
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
import KeyboardShortcuts
|
|
3
3
|
|
|
4
|
-
struct SettingsView: View {
|
|
5
|
-
@ObservedObject var engine: RecordingEngine
|
|
6
|
-
@ObservedObject var shortcuts: VoiceShortcuts
|
|
7
|
-
@ObservedObject var projectStore: ProjectStore
|
|
4
|
+
public struct SettingsView: View {
|
|
5
|
+
@ObservedObject public var engine: RecordingEngine
|
|
6
|
+
@ObservedObject public var shortcuts: VoiceShortcuts
|
|
7
|
+
@ObservedObject public var projectStore: ProjectStore
|
|
8
|
+
@AppStorage("openAIAPIKey") private var openAIAPIKey = ""
|
|
9
|
+
|
|
10
|
+
public init(engine: RecordingEngine, shortcuts: VoiceShortcuts, projectStore: ProjectStore) {
|
|
11
|
+
self.engine = engine
|
|
12
|
+
self.shortcuts = shortcuts
|
|
13
|
+
self.projectStore = projectStore
|
|
14
|
+
}
|
|
8
15
|
|
|
9
|
-
var body: some View {
|
|
16
|
+
public var body: some View {
|
|
10
17
|
TabView {
|
|
11
18
|
generalTab.tabItem { Label("General", systemImage: "gear") }
|
|
12
19
|
projectsTab.tabItem { Label("Projects", systemImage: "folder") }
|
|
@@ -19,6 +26,13 @@ struct SettingsView: View {
|
|
|
19
26
|
|
|
20
27
|
private var generalTab: some View {
|
|
21
28
|
Form {
|
|
29
|
+
Section("OpenAI") {
|
|
30
|
+
SecureField("API key", text: $openAIAPIKey)
|
|
31
|
+
.textFieldStyle(.roundedBorder)
|
|
32
|
+
Text("Used for realtime transcription in the menu bar app.")
|
|
33
|
+
.foregroundStyle(.secondary)
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
Section("Recording Shortcut") {
|
|
23
37
|
HStack {
|
|
24
38
|
Text("Shortcut")
|
|
@@ -2,12 +2,12 @@ import SwiftUI
|
|
|
2
2
|
|
|
3
3
|
// MARK: - Voice Shortcut
|
|
4
4
|
|
|
5
|
-
struct VoiceShortcut: Identifiable, Codable {
|
|
6
|
-
let id: UUID
|
|
7
|
-
var trigger: String // e.g. "add disclaimer"
|
|
8
|
-
var content: String // text to insert when trigger is spoken
|
|
5
|
+
public struct VoiceShortcut: Identifiable, Codable {
|
|
6
|
+
public let id: UUID
|
|
7
|
+
public var trigger: String // e.g. "add disclaimer"
|
|
8
|
+
public var content: String // text to insert when trigger is spoken
|
|
9
9
|
|
|
10
|
-
init(trigger: String, content: String) {
|
|
10
|
+
public init(trigger: String, content: String) {
|
|
11
11
|
self.id = UUID()
|
|
12
12
|
self.trigger = trigger
|
|
13
13
|
self.content = content
|
|
@@ -17,15 +17,15 @@ struct VoiceShortcut: Identifiable, Codable {
|
|
|
17
17
|
// MARK: - Voice Shortcuts Manager
|
|
18
18
|
|
|
19
19
|
@MainActor
|
|
20
|
-
final class VoiceShortcuts: ObservableObject {
|
|
21
|
-
@Published var shortcuts: [VoiceShortcut] = []
|
|
20
|
+
public final class VoiceShortcuts: ObservableObject {
|
|
21
|
+
@Published public var shortcuts: [VoiceShortcut] = []
|
|
22
22
|
|
|
23
23
|
private let storageURL: URL = {
|
|
24
24
|
let home = FileManager.default.homeDirectoryForCurrentUser
|
|
25
25
|
return home.appendingPathComponent(".hasna/recordings/voice-shortcuts.json")
|
|
26
26
|
}()
|
|
27
27
|
|
|
28
|
-
init() {
|
|
28
|
+
public init() {
|
|
29
29
|
load()
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -64,6 +64,10 @@ final class VoiceShortcuts: ObservableObject {
|
|
|
64
64
|
private func save() {
|
|
65
65
|
do {
|
|
66
66
|
let data = try JSONEncoder().encode(shortcuts)
|
|
67
|
+
try FileManager.default.createDirectory(
|
|
68
|
+
at: storageURL.deletingLastPathComponent(),
|
|
69
|
+
withIntermediateDirectories: true
|
|
70
|
+
)
|
|
67
71
|
try data.write(to: storageURL, options: .atomic)
|
|
68
72
|
} catch {
|
|
69
73
|
print("Failed to save voice shortcuts: \(error)")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
@testable import RecordingsLib
|
|
3
|
+
|
|
4
|
+
struct CLIRunnerTests {
|
|
5
|
+
@Test("parseError detects ERROR prefix")
|
|
6
|
+
func parseError() {
|
|
7
|
+
#expect(CLIRunner.parseError("ERROR: OpenAI API key not configured on this Mac") == "OpenAI API key not configured on this Mac")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@Test("parseError returns nil for normal output")
|
|
11
|
+
func noError() {
|
|
12
|
+
#expect(CLIRunner.parseError("Hello world") == nil)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Test("parseError handles API key error")
|
|
16
|
+
func apiKeyError() {
|
|
17
|
+
#expect(CLIRunner.parseError("ERROR: OpenAI API key not configured") == "OpenAI API key not configured on this Mac")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Test("parseError truncates long messages to 120 chars")
|
|
21
|
+
func truncation() {
|
|
22
|
+
let longMsg = String(repeating: "a", count: 200)
|
|
23
|
+
let input = "ERROR: \(longMsg)"
|
|
24
|
+
let result = CLIRunner.parseError(input)!
|
|
25
|
+
#expect(result.count <= 120)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test("parseJSON extracts raw_text from JSON")
|
|
29
|
+
func parseJSONRawText() {
|
|
30
|
+
let output = """
|
|
31
|
+
{"raw_text": "Hello world", "processed_text": null}
|
|
32
|
+
"""
|
|
33
|
+
#expect(CLIRunner.parseJSON(output) == "Hello world")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test("parseJSON prefers processed_text over raw_text")
|
|
37
|
+
func parseJSONProcessedText() {
|
|
38
|
+
let output = """
|
|
39
|
+
{"raw_text": "Hello world", "processed_text": "Hello World (enhanced)"}
|
|
40
|
+
"""
|
|
41
|
+
#expect(CLIRunner.parseJSON(output) == "Hello World (enhanced)")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test("parseJSON falls back to plain text")
|
|
45
|
+
func parseJSONFallback() {
|
|
46
|
+
let output = "Transcribing...\nHello world\nSaved to file"
|
|
47
|
+
#expect(CLIRunner.parseJSON(output) == "Hello world")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Test("parseJSON returns nil for empty output")
|
|
51
|
+
func emptyOutput() {
|
|
52
|
+
#expect(CLIRunner.parseJSON("") == nil)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test("parseJSON handles empty transcription")
|
|
56
|
+
func emptyTranscription() {
|
|
57
|
+
let output = """
|
|
58
|
+
{"raw_text": "", "processed_text": ""}
|
|
59
|
+
"""
|
|
60
|
+
// Both are empty, so it should fall back to plain text extraction
|
|
61
|
+
#expect(CLIRunner.parseJSON(output) == nil)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Testing
|
|
3
|
+
@testable import RecordingsLib
|
|
4
|
+
|
|
5
|
+
struct NativeAppDiagnosticsTests {
|
|
6
|
+
@Test("Native app log writes to recordings log file")
|
|
7
|
+
func logWritesFile() throws {
|
|
8
|
+
let home = try makeHome()
|
|
9
|
+
|
|
10
|
+
NativeAppLog.write("diagnostic-test", homePath: home)
|
|
11
|
+
|
|
12
|
+
let path = "\(home)/.hasna/recordings/Recordings.log"
|
|
13
|
+
let text = try String(contentsOfFile: path, encoding: .utf8)
|
|
14
|
+
#expect(text.contains("diagnostic-test"))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private func makeHome() throws -> String {
|
|
18
|
+
let url = FileManager.default.temporaryDirectory
|
|
19
|
+
.appendingPathComponent("recordings-diagnostics-tests-\(UUID().uuidString)")
|
|
20
|
+
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
21
|
+
return url.path
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import Testing
|
|
3
|
+
@testable import RecordingsLib
|
|
4
|
+
|
|
5
|
+
struct NativePCMRecorderTests {
|
|
6
|
+
@Test("Realtime output format is OpenAI-compatible 24 kHz mono PCM16")
|
|
7
|
+
func realtimeOutputFormat() throws {
|
|
8
|
+
let format = try #require(NativePCMRecorder.realtimeOutputFormat())
|
|
9
|
+
|
|
10
|
+
#expect(format.commonFormat == .pcmFormatInt16)
|
|
11
|
+
#expect(format.sampleRate == 24_000)
|
|
12
|
+
#expect(format.channelCount == 1)
|
|
13
|
+
#expect(format.isInterleaved)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Test("Extracts PCM16 bytes from audio buffer")
|
|
17
|
+
func extractPCM16Data() throws {
|
|
18
|
+
let format = try #require(NativePCMRecorder.realtimeOutputFormat())
|
|
19
|
+
let buffer = try #require(AVAudioPCMBuffer(pcmFormat: format, frameCapacity: 4))
|
|
20
|
+
buffer.frameLength = 4
|
|
21
|
+
|
|
22
|
+
let samples = try #require(buffer.int16ChannelData?[0])
|
|
23
|
+
samples[0] = 1
|
|
24
|
+
samples[1] = -2
|
|
25
|
+
samples[2] = 3
|
|
26
|
+
samples[3] = -4
|
|
27
|
+
|
|
28
|
+
let data = NativePCMRecorder.extractPCM16Data(from: buffer)
|
|
29
|
+
|
|
30
|
+
#expect(data.count == 8)
|
|
31
|
+
#expect(data.withUnsafeBytes { $0.load(as: Int16.self) } == 1)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Testing
|
|
3
|
+
@testable import RecordingsLib
|
|
4
|
+
|
|
5
|
+
struct OpenAIAPIKeyStoreTests {
|
|
6
|
+
@Test("Environment key has highest priority")
|
|
7
|
+
func environmentKeyWins() {
|
|
8
|
+
let key = OpenAIAPIKeyStore.load(
|
|
9
|
+
homePath: "/tmp/recordings-missing-home",
|
|
10
|
+
environment: ["OPENAI_API_KEY": "env-key"],
|
|
11
|
+
userDefaultKey: "stored-key"
|
|
12
|
+
)
|
|
13
|
+
#expect(key == "env-key")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Test("User default key is used before config file")
|
|
17
|
+
func userDefaultKeyWins() throws {
|
|
18
|
+
let home = try makeHome()
|
|
19
|
+
try writeConfig(home: home, ["openai_api_key": "config-key"])
|
|
20
|
+
|
|
21
|
+
let key = OpenAIAPIKeyStore.load(
|
|
22
|
+
homePath: home.path,
|
|
23
|
+
environment: [:],
|
|
24
|
+
userDefaultKey: "stored-key"
|
|
25
|
+
)
|
|
26
|
+
#expect(key == "stored-key")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test("Config file key is loaded from installed app data")
|
|
30
|
+
func configFileKey() throws {
|
|
31
|
+
let home = try makeHome()
|
|
32
|
+
try writeConfig(home: home, ["openai_api_key": "config-key"])
|
|
33
|
+
|
|
34
|
+
let key = OpenAIAPIKeyStore.load(
|
|
35
|
+
homePath: home.path,
|
|
36
|
+
environment: [:],
|
|
37
|
+
userDefaultKey: nil
|
|
38
|
+
)
|
|
39
|
+
#expect(key == "config-key")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test("Config file can reference environment variable")
|
|
43
|
+
func configEnvReference() throws {
|
|
44
|
+
let home = try makeHome()
|
|
45
|
+
try writeConfig(home: home, ["openai_api_key": "$RECORDINGS_API_KEY"])
|
|
46
|
+
|
|
47
|
+
let key = OpenAIAPIKeyStore.load(
|
|
48
|
+
homePath: home.path,
|
|
49
|
+
environment: ["RECORDINGS_API_KEY": "referenced-key"],
|
|
50
|
+
userDefaultKey: nil
|
|
51
|
+
)
|
|
52
|
+
#expect(key == "referenced-key")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test("Secrets env files are searched recursively")
|
|
56
|
+
func recursiveSecrets() throws {
|
|
57
|
+
let home = try makeHome()
|
|
58
|
+
let secretDir = home
|
|
59
|
+
.appendingPathComponent(".secrets")
|
|
60
|
+
.appendingPathComponent("hasnaxyz")
|
|
61
|
+
.appendingPathComponent("openai")
|
|
62
|
+
try FileManager.default.createDirectory(at: secretDir, withIntermediateDirectories: true)
|
|
63
|
+
try "export OPENAI_API_KEY='secret-key'\n".write(
|
|
64
|
+
to: secretDir.appendingPathComponent("live.env"),
|
|
65
|
+
atomically: true,
|
|
66
|
+
encoding: .utf8
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
let key = OpenAIAPIKeyStore.load(
|
|
70
|
+
homePath: home.path,
|
|
71
|
+
environment: [:],
|
|
72
|
+
userDefaultKey: nil
|
|
73
|
+
)
|
|
74
|
+
#expect(key == "secret-key")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private func makeHome() throws -> URL {
|
|
78
|
+
let url = FileManager.default.temporaryDirectory
|
|
79
|
+
.appendingPathComponent("recordings-key-store-\(UUID().uuidString)")
|
|
80
|
+
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
81
|
+
return url
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func writeConfig(home: URL, _ config: [String: String]) throws {
|
|
85
|
+
let configDir = home
|
|
86
|
+
.appendingPathComponent(".hasna")
|
|
87
|
+
.appendingPathComponent("recordings")
|
|
88
|
+
try FileManager.default.createDirectory(at: configDir, withIntermediateDirectories: true)
|
|
89
|
+
let data = try JSONSerialization.data(withJSONObject: config, options: [.prettyPrinted])
|
|
90
|
+
try data.write(to: configDir.appendingPathComponent("config.json"))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
@testable import RecordingsLib
|
|
3
|
+
|
|
4
|
+
struct ProjectStoreTests {
|
|
5
|
+
@Test("matchProject by bundle ID")
|
|
6
|
+
func matchByBundleId() {
|
|
7
|
+
let projects = [
|
|
8
|
+
RecProject(name: "MyApp", appBundleIds: ["com.example.myapp"]),
|
|
9
|
+
RecProject(name: "Other", appBundleIds: ["com.example.other"]),
|
|
10
|
+
]
|
|
11
|
+
let result = ProjectStore.matchProject(windowTitle: nil, bundleId: "com.example.myapp", projects: projects)
|
|
12
|
+
#expect(result?.name == "MyApp")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Test("matchProject by window title containing project name")
|
|
16
|
+
func matchByWindowTitle() {
|
|
17
|
+
let projects = [
|
|
18
|
+
RecProject(name: "Hasna", path: nil),
|
|
19
|
+
RecProject(name: "Other", path: nil),
|
|
20
|
+
]
|
|
21
|
+
let result = ProjectStore.matchProject(windowTitle: "Hasna — Takumi", bundleId: nil, projects: projects)
|
|
22
|
+
#expect(result?.name == "Hasna")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test("matchProject by path component in window title")
|
|
26
|
+
func matchByPathComponent() {
|
|
27
|
+
let projects = [
|
|
28
|
+
RecProject(name: "Backend API", path: "/projects/backend-api"),
|
|
29
|
+
RecProject(name: "Other", path: nil),
|
|
30
|
+
]
|
|
31
|
+
let result = ProjectStore.matchProject(windowTitle: "backend-api — VSCode", bundleId: nil, projects: projects)
|
|
32
|
+
#expect(result?.name == "Backend API")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test("matchProject returns nil when no match")
|
|
36
|
+
func noMatch() {
|
|
37
|
+
let projects = [
|
|
38
|
+
RecProject(name: "ProjectA", appBundleIds: ["com.a"]),
|
|
39
|
+
]
|
|
40
|
+
let result = ProjectStore.matchProject(windowTitle: "ProjectB", bundleId: "com.b", projects: projects)
|
|
41
|
+
#expect(result == nil)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test("matchProject returns nil for empty projects")
|
|
45
|
+
func emptyProjects() {
|
|
46
|
+
let result = ProjectStore.matchProject(windowTitle: "Anything", bundleId: "com.any", projects: [])
|
|
47
|
+
#expect(result == nil)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Test("matchProject is case-insensitive")
|
|
51
|
+
func caseInsensitive() {
|
|
52
|
+
let projects = [
|
|
53
|
+
RecProject(name: "myapp", path: nil),
|
|
54
|
+
]
|
|
55
|
+
let result = ProjectStore.matchProject(windowTitle: "MyApp - Editor", bundleId: nil, projects: projects)
|
|
56
|
+
#expect(result?.name == "myapp")
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
@testable import RecordingsLib
|
|
3
|
+
|
|
4
|
+
// MARK: - RealtimeTranscriptionClient Event Parsing Tests
|
|
5
|
+
|
|
6
|
+
struct RealtimeTranscriptionTests {
|
|
7
|
+
@Test("Model ID is set to latest gpt-4o-transcribe")
|
|
8
|
+
func modelID() {
|
|
9
|
+
#expect(RealtimeTranscriptionClient.modelID == "gpt-4o-transcribe")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Test("Parses transcription delta events")
|
|
13
|
+
func parseDelta() {
|
|
14
|
+
let deltaJSON = """
|
|
15
|
+
{"type":"conversation.item.input_audio_transcription.delta","delta":"Hello "}
|
|
16
|
+
"""
|
|
17
|
+
#expect(RealtimeTranscriptionClient.parseDeltaTestHelper(deltaJSON) == "Hello ")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Test("Parses transcription completed event")
|
|
21
|
+
func parseCompleted() {
|
|
22
|
+
let completedJSON = """
|
|
23
|
+
{"type":"conversation.item.input_audio_transcription.completed","transcript":"Hello world"}
|
|
24
|
+
"""
|
|
25
|
+
#expect(RealtimeTranscriptionClient.parseDeltaTestHelper(completedJSON) == "Hello world")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test("Returns nil for non-transcription events")
|
|
29
|
+
func ignoreOtherEvents() {
|
|
30
|
+
let sessionJSON = """
|
|
31
|
+
{"type":"session.created","session":{"id":"abc"}}
|
|
32
|
+
"""
|
|
33
|
+
#expect(RealtimeTranscriptionClient.parseDeltaTestHelper(sessionJSON) == nil)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test("Returns nil for malformed JSON")
|
|
37
|
+
func malformedJSON() {
|
|
38
|
+
#expect(RealtimeTranscriptionClient.parseDeltaTestHelper("not json") == nil)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Test("Returns nil for empty delta")
|
|
42
|
+
func emptyDelta() {
|
|
43
|
+
let emptyJSON = """
|
|
44
|
+
{"type":"conversation.item.input_audio_transcription.delta","delta":""}
|
|
45
|
+
"""
|
|
46
|
+
// Empty string delta is still valid — client should handle it
|
|
47
|
+
#expect(RealtimeTranscriptionClient.parseDeltaTestHelper(emptyJSON) == "")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Test("Detects error events")
|
|
51
|
+
func detectError() {
|
|
52
|
+
let errorJSON = """
|
|
53
|
+
{"type":"error","error":{"message":"Invalid API key","code":401}}
|
|
54
|
+
"""
|
|
55
|
+
#expect(RealtimeTranscriptionClient.isSessionErrorTestHelper(errorJSON) == true)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Test("Does not flag normal events as errors")
|
|
59
|
+
func noFalsePositives() {
|
|
60
|
+
#expect(RealtimeTranscriptionClient.isSessionErrorTestHelper(
|
|
61
|
+
"{\"type\":\"conversation.item.input_audio_transcription.delta\"}"
|
|
62
|
+
) == false)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Test("Parse error message from error event")
|
|
66
|
+
func parseErrorMessage() {
|
|
67
|
+
let errorJSON = """
|
|
68
|
+
{"type":"error","error":{"message":"Model not found","code":404}}
|
|
69
|
+
"""
|
|
70
|
+
#expect(RealtimeTranscriptionClient.parseErrorTestHelper(errorJSON) == "Model not found")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test("Builds strict verbatim prompt with vocabulary context")
|
|
74
|
+
func buildPrompt() {
|
|
75
|
+
let prompt = RealtimeTranscriptionClient.buildPromptTestHelper("Alumia, Takumi")
|
|
76
|
+
#expect(prompt.contains("verbatim"))
|
|
77
|
+
#expect(prompt.contains("Do not summarize"))
|
|
78
|
+
#expect(prompt.contains("vocabulary context"))
|
|
79
|
+
#expect(prompt.contains("Alumia"))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Test("Builds realtime transcription session update event")
|
|
83
|
+
func buildSessionUpdateEvent() {
|
|
84
|
+
let event = RealtimeTranscriptionClient.sessionUpdateTestHelper(prompt: "Use Alumia as vocabulary", language: "en")
|
|
85
|
+
#expect(event["type"] as? String == "session.update")
|
|
86
|
+
|
|
87
|
+
let session = event["session"] as? [String: Any]
|
|
88
|
+
#expect(session?["type"] as? String == "transcription")
|
|
89
|
+
|
|
90
|
+
let audio = session?["audio"] as? [String: Any]
|
|
91
|
+
let input = audio?["input"] as? [String: Any]
|
|
92
|
+
let format = input?["format"] as? [String: Any]
|
|
93
|
+
#expect(format?["type"] as? String == "audio/pcm")
|
|
94
|
+
#expect(format?["rate"] as? Int == 24_000)
|
|
95
|
+
|
|
96
|
+
let transcription = input?["transcription"] as? [String: Any]
|
|
97
|
+
#expect(transcription?["model"] as? String == "gpt-4o-transcribe")
|
|
98
|
+
#expect(transcription?["prompt"] as? String == "Use Alumia as vocabulary")
|
|
99
|
+
#expect(transcription?["language"] as? String == "en")
|
|
100
|
+
|
|
101
|
+
let turnDetection = input?["turn_detection"] as? [String: Any]
|
|
102
|
+
#expect(turnDetection?["type"] as? String == "server_vad")
|
|
103
|
+
|
|
104
|
+
let include = session?["include"] as? [String]
|
|
105
|
+
#expect(include?.contains("item.input_audio_transcription.logprobs") == true)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Test("Joins transcript parts without dropping spoken text")
|
|
109
|
+
func joinParts() {
|
|
110
|
+
let text = RealtimeTranscriptionClient.joinTranscriptPartsTestHelper(["Hello", "world.", " Next"])
|
|
111
|
+
#expect(text == "Hello world. Next")
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/bin/bash
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
2
|
# Build Recordings.app for macOS 26
|
|
3
3
|
# Usage: ./build.sh [debug|release]
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
9
9
|
cd "$SCRIPT_DIR"
|
|
10
10
|
|
|
11
11
|
echo "Building Recordings.app ($MODE)..."
|
|
12
|
-
swift build -c "$MODE"
|
|
12
|
+
swift build -c "$MODE" --product App
|
|
13
13
|
|
|
14
14
|
# Create .app bundle
|
|
15
15
|
BUILD_DIR=".build/$MODE"
|
|
@@ -21,14 +21,14 @@ rm -rf "$APP_DIR"
|
|
|
21
21
|
mkdir -p "$MACOS"
|
|
22
22
|
|
|
23
23
|
# Copy binary
|
|
24
|
-
cp "$BUILD_DIR/
|
|
24
|
+
cp "$BUILD_DIR/App" "$MACOS/Recordings"
|
|
25
25
|
|
|
26
26
|
# Copy Info.plist
|
|
27
|
-
cp
|
|
27
|
+
cp RecordingsLib/Info.plist "$CONTENTS/Info.plist"
|
|
28
28
|
|
|
29
29
|
# Copy entitlements (for codesigning)
|
|
30
|
-
if [ -f
|
|
31
|
-
codesign --force --sign - --entitlements
|
|
30
|
+
if [ -f RecordingsLib/Recordings.entitlements ]; then
|
|
31
|
+
codesign --force --sign - --entitlements RecordingsLib/Recordings.entitlements "$APP_DIR" 2>/dev/null || true
|
|
32
32
|
fi
|
|
33
33
|
|
|
34
34
|
echo "✓ Built $APP_DIR"
|