@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
package/dist/types/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface Recording {
|
|
|
15
15
|
goal: string | null;
|
|
16
16
|
role: string | null;
|
|
17
17
|
task_list_id: string | null;
|
|
18
|
+
machine_id: string | null;
|
|
18
19
|
metadata: Record<string, unknown>;
|
|
19
20
|
created_at: string;
|
|
20
21
|
}
|
|
@@ -35,6 +36,7 @@ export interface CreateRecordingInput {
|
|
|
35
36
|
goal?: string;
|
|
36
37
|
role?: string;
|
|
37
38
|
task_list_id?: string;
|
|
39
|
+
machine_id?: string;
|
|
38
40
|
metadata?: Record<string, unknown>;
|
|
39
41
|
}
|
|
40
42
|
export interface RecordingFilter {
|
|
@@ -78,6 +80,7 @@ export interface RecordingsConfig {
|
|
|
78
80
|
hotkey: string;
|
|
79
81
|
auto_enhance: boolean;
|
|
80
82
|
enhance_triggers: string[];
|
|
83
|
+
keyword_transforms: Record<string, string>;
|
|
81
84
|
db_path: string;
|
|
82
85
|
audio_dir: string;
|
|
83
86
|
max_recording_seconds: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,cAAc,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,UAAU,CAAC;AAEhD,MAAM,WAAW,oBAAoB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAID,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAID,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAID,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,EAAE,EAAE,MAAM;CAIvB;AAED,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,WAAW,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/recordings",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/hasna/recordings.git"
|
|
9
|
+
},
|
|
6
10
|
"main": "dist/index.js",
|
|
7
11
|
"types": "dist/index.d.ts",
|
|
8
12
|
"bin": {
|
|
@@ -25,8 +29,21 @@
|
|
|
25
29
|
"test:coverage": "bun test --coverage",
|
|
26
30
|
"dev:cli": "bun run src/cli/index.ts",
|
|
27
31
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
28
|
-
"
|
|
32
|
+
"prepack": "bun run build",
|
|
33
|
+
"postinstall": "bash scripts/install_macos_app.sh --postinstall"
|
|
29
34
|
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/",
|
|
37
|
+
"scripts/",
|
|
38
|
+
"src/native/Recordings/App/",
|
|
39
|
+
"src/native/Recordings/RecordingsLib/",
|
|
40
|
+
"src/native/Recordings/RecordingsTests/",
|
|
41
|
+
"src/native/Recordings/Package.swift",
|
|
42
|
+
"src/native/Recordings/Package.resolved",
|
|
43
|
+
"src/native/Recordings/build.sh",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
30
47
|
"dependencies": {
|
|
31
48
|
"@hasna/cloud": "^0.1.24",
|
|
32
49
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
@@ -45,4 +62,4 @@
|
|
|
45
62
|
},
|
|
46
63
|
"license": "Apache-2.0",
|
|
47
64
|
"author": "Hasna <andrei@hasna.com>"
|
|
48
|
-
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -u
|
|
3
|
+
|
|
4
|
+
MODE="release"
|
|
5
|
+
POSTINSTALL=0
|
|
6
|
+
|
|
7
|
+
while [ "$#" -gt 0 ]; do
|
|
8
|
+
case "$1" in
|
|
9
|
+
--mode)
|
|
10
|
+
MODE="${2:-release}"
|
|
11
|
+
shift 2
|
|
12
|
+
;;
|
|
13
|
+
--postinstall)
|
|
14
|
+
POSTINSTALL=1
|
|
15
|
+
shift
|
|
16
|
+
;;
|
|
17
|
+
*)
|
|
18
|
+
echo "Unknown argument: $1" >&2
|
|
19
|
+
exit 2
|
|
20
|
+
;;
|
|
21
|
+
esac
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
case "$MODE" in
|
|
25
|
+
debug|release) ;;
|
|
26
|
+
*)
|
|
27
|
+
echo "Mode must be debug or release" >&2
|
|
28
|
+
exit 2
|
|
29
|
+
;;
|
|
30
|
+
esac
|
|
31
|
+
|
|
32
|
+
PACKAGE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
33
|
+
DATA_DIR="${HOME}/.hasna/recordings"
|
|
34
|
+
NATIVE_DIR="${PACKAGE_ROOT}/src/native/Recordings"
|
|
35
|
+
APP_SOURCE="${NATIVE_DIR}/.build/${MODE}/Recordings.app"
|
|
36
|
+
APP_DEST="${DATA_DIR}/Recordings.app"
|
|
37
|
+
|
|
38
|
+
mkdir -p "${DATA_DIR}/audio"
|
|
39
|
+
|
|
40
|
+
if [ "$(uname -s)" != "Darwin" ]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
warn_or_fail() {
|
|
45
|
+
local message="$1"
|
|
46
|
+
if [ "$POSTINSTALL" -eq 1 ]; then
|
|
47
|
+
echo "Recordings.app was not installed: ${message}" >&2
|
|
48
|
+
echo "Run 'recordings app install' after fixing this machine." >&2
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
echo "${message}" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if ! command -v swift >/dev/null 2>&1; then
|
|
56
|
+
warn_or_fail "Swift toolchain not found"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
if [ ! -d "$NATIVE_DIR" ]; then
|
|
60
|
+
warn_or_fail "native app sources are missing from package: ${NATIVE_DIR}"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
(
|
|
64
|
+
cd "$NATIVE_DIR" || exit 1
|
|
65
|
+
./build.sh "$MODE"
|
|
66
|
+
) || warn_or_fail "native app build failed"
|
|
67
|
+
|
|
68
|
+
if [ ! -d "$APP_SOURCE" ]; then
|
|
69
|
+
warn_or_fail "native build did not produce ${APP_SOURCE}"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
rm -rf "$APP_DEST"
|
|
73
|
+
mkdir -p "$DATA_DIR"
|
|
74
|
+
cp -R "$APP_SOURCE" "$APP_DEST" || warn_or_fail "failed to copy app bundle"
|
|
75
|
+
|
|
76
|
+
echo "Installed Recordings.app from package: ${APP_DEST}"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
@preconcurrency import Cocoa
|
|
2
2
|
import SwiftUI
|
|
3
3
|
import KeyboardShortcuts
|
|
4
|
+
import RecordingsLib
|
|
4
5
|
|
|
5
6
|
@main
|
|
6
7
|
struct RecordingsApp: App {
|
|
@@ -18,7 +19,10 @@ struct RecordingsApp: App {
|
|
|
18
19
|
MenuBarExtra {
|
|
19
20
|
MenuBarPopover(engine: engine, shortcuts: shortcuts, projectStore: projectStore)
|
|
20
21
|
.frame(width: 320, height: 420)
|
|
21
|
-
.onAppear {
|
|
22
|
+
.onAppear {
|
|
23
|
+
engine.projectStore = projectStore
|
|
24
|
+
engine.voiceShortcuts = shortcuts
|
|
25
|
+
}
|
|
22
26
|
} label: {
|
|
23
27
|
if engine.isRecording {
|
|
24
28
|
Image(systemName: "record.circle.fill")
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
{
|
|
2
|
-
"originHash" : "
|
|
2
|
+
"originHash" : "a63dc0aa4ab0839e19cd7644cfd2bb42729aafbf9ef920adeb5c3ac856b7701e",
|
|
3
3
|
"pins" : [
|
|
4
4
|
{
|
|
5
5
|
"identity" : "keyboardshortcuts",
|
|
6
6
|
"kind" : "remoteSourceControl",
|
|
7
7
|
"location" : "https://github.com/sindresorhus/KeyboardShortcuts",
|
|
8
8
|
"state" : {
|
|
9
|
-
"revision" : "
|
|
10
|
-
"version" : "
|
|
9
|
+
"revision" : "de5b143889b9aa90a997b18e311527095dfb28e0",
|
|
10
|
+
"version" : "1.12.0"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"identity" : "swift-syntax",
|
|
15
|
+
"kind" : "remoteSourceControl",
|
|
16
|
+
"location" : "https://github.com/swiftlang/swift-syntax.git",
|
|
17
|
+
"state" : {
|
|
18
|
+
"revision" : "0687f71944021d616d34d922343dcef086855920",
|
|
19
|
+
"version" : "600.0.1"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"identity" : "swift-testing",
|
|
24
|
+
"kind" : "remoteSourceControl",
|
|
25
|
+
"location" : "https://github.com/apple/swift-testing.git",
|
|
26
|
+
"state" : {
|
|
27
|
+
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
|
|
28
|
+
"version" : "0.99.0"
|
|
11
29
|
}
|
|
12
30
|
}
|
|
13
31
|
],
|
|
@@ -8,14 +8,25 @@ let package = Package(
|
|
|
8
8
|
.macOS(.v26)
|
|
9
9
|
],
|
|
10
10
|
dependencies: [
|
|
11
|
-
.package(url: "https://github.com/sindresorhus/KeyboardShortcuts",
|
|
11
|
+
.package(url: "https://github.com/sindresorhus/KeyboardShortcuts", exact: "1.12.0"),
|
|
12
|
+
.package(url: "https://github.com/apple/swift-testing.git", .upToNextMinor(from: "0.99.0")),
|
|
12
13
|
],
|
|
13
14
|
targets: [
|
|
14
|
-
.
|
|
15
|
-
name: "
|
|
15
|
+
.target(
|
|
16
|
+
name: "RecordingsLib",
|
|
16
17
|
dependencies: ["KeyboardShortcuts"],
|
|
17
|
-
path: "
|
|
18
|
+
path: "RecordingsLib",
|
|
18
19
|
exclude: ["Info.plist", "Recordings.entitlements"]
|
|
19
|
-
)
|
|
20
|
+
),
|
|
21
|
+
.executableTarget(
|
|
22
|
+
name: "App",
|
|
23
|
+
dependencies: ["RecordingsLib"],
|
|
24
|
+
path: "App"
|
|
25
|
+
),
|
|
26
|
+
.testTarget(
|
|
27
|
+
name: "RecordingsTests",
|
|
28
|
+
dependencies: ["RecordingsLib", .product(name: "Testing", package: "swift-testing")],
|
|
29
|
+
path: "RecordingsTests"
|
|
30
|
+
),
|
|
20
31
|
]
|
|
21
32
|
)
|
|
@@ -8,15 +8,21 @@
|
|
|
8
8
|
<string>Recordings</string>
|
|
9
9
|
<key>CFBundleDisplayName</key>
|
|
10
10
|
<string>Recordings</string>
|
|
11
|
+
<key>CFBundleExecutable</key>
|
|
12
|
+
<string>Recordings</string>
|
|
11
13
|
<key>CFBundleIdentifier</key>
|
|
12
14
|
<string>com.hasna.recordings</string>
|
|
13
15
|
<key>CFBundleVersion</key>
|
|
14
16
|
<string>1</string>
|
|
17
|
+
<key>CFBundlePackageType</key>
|
|
18
|
+
<string>APPL</string>
|
|
15
19
|
<key>CFBundleShortVersionString</key>
|
|
16
20
|
<string>0.1.0</string>
|
|
17
21
|
<key>LSMinimumSystemVersion</key>
|
|
18
22
|
<string>26.0</string>
|
|
19
23
|
<key>NSMicrophoneUsageDescription</key>
|
|
20
24
|
<string>Recordings needs microphone access to record and transcribe your speech.</string>
|
|
25
|
+
<key>NSAppleEventsUsageDescription</key>
|
|
26
|
+
<string>Recordings needs Apple Events access to paste transcribed text into your active app.</string>
|
|
21
27
|
</dict>
|
|
22
28
|
</plist>
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
import KeyboardShortcuts
|
|
3
3
|
|
|
4
|
-
struct MenuBarPopover: View {
|
|
5
|
-
@ObservedObject var engine: RecordingEngine
|
|
6
|
-
@ObservedObject var shortcuts: VoiceShortcuts
|
|
7
|
-
@ObservedObject var projectStore: ProjectStore
|
|
4
|
+
public struct MenuBarPopover: View {
|
|
5
|
+
@ObservedObject public var engine: RecordingEngine
|
|
6
|
+
@ObservedObject public var shortcuts: VoiceShortcuts
|
|
7
|
+
@ObservedObject public var projectStore: ProjectStore
|
|
8
8
|
@State private var copiedIndex: Int?
|
|
9
9
|
@State private var filterProjectId: String?
|
|
10
10
|
|
|
11
|
+
public init(engine: RecordingEngine, shortcuts: VoiceShortcuts, projectStore: ProjectStore) {
|
|
12
|
+
self.engine = engine
|
|
13
|
+
self.shortcuts = shortcuts
|
|
14
|
+
self.projectStore = projectStore
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
private var filteredTranscriptions: [TranscriptionResult] {
|
|
12
18
|
guard let filter = filterProjectId else {
|
|
13
19
|
return engine.recentTranscriptions
|
|
@@ -18,13 +24,15 @@ struct MenuBarPopover: View {
|
|
|
18
24
|
return engine.recentTranscriptions.filter { $0.projectId == filter }
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
var body: some View {
|
|
27
|
+
public var body: some View {
|
|
22
28
|
VStack(spacing: 0) {
|
|
23
29
|
header
|
|
24
30
|
.padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 8)
|
|
25
31
|
Divider()
|
|
32
|
+
modePicker
|
|
33
|
+
.padding(.horizontal, 16).padding(.top, 10)
|
|
26
34
|
recordingArea
|
|
27
|
-
.padding(.horizontal, 16).padding(.
|
|
35
|
+
.padding(.horizontal, 16).padding(.top, 8).padding(.bottom, 12)
|
|
28
36
|
Divider()
|
|
29
37
|
if !engine.recentTranscriptions.isEmpty {
|
|
30
38
|
filterBar
|
|
@@ -85,9 +93,20 @@ struct MenuBarPopover: View {
|
|
|
85
93
|
|
|
86
94
|
// MARK: - Recording Area
|
|
87
95
|
|
|
96
|
+
private var modePicker: some View {
|
|
97
|
+
Picker("Mode", selection: $engine.mode) {
|
|
98
|
+
ForEach(RecordingMode.allCases) { mode in
|
|
99
|
+
Image(systemName: mode.icon)
|
|
100
|
+
.tag(mode)
|
|
101
|
+
.help(mode.rawValue)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
.pickerStyle(.segmented)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@ViewBuilder
|
|
88
108
|
private var recordingArea: some View {
|
|
89
|
-
|
|
90
|
-
if engine.isRecording {
|
|
109
|
+
if engine.isRecording {
|
|
91
110
|
VStack(spacing: 6) {
|
|
92
111
|
HStack {
|
|
93
112
|
Circle().fill(.red).frame(width: 8, height: 8)
|
|
@@ -97,6 +116,18 @@ struct MenuBarPopover: View {
|
|
|
97
116
|
Button("Stop") { engine.stopAndTranscribe() }
|
|
98
117
|
.controlSize(.small)
|
|
99
118
|
}
|
|
119
|
+
// Live streaming transcription text
|
|
120
|
+
if !engine.liveTranscriptionText.isEmpty {
|
|
121
|
+
Text(engine.liveTranscriptionText)
|
|
122
|
+
.font(.callout)
|
|
123
|
+
.foregroundStyle(.primary)
|
|
124
|
+
.multilineTextAlignment(.leading)
|
|
125
|
+
.lineLimit(3)
|
|
126
|
+
.padding(.vertical, 4)
|
|
127
|
+
Text("Listening...")
|
|
128
|
+
.font(.caption)
|
|
129
|
+
.foregroundStyle(.green)
|
|
130
|
+
}
|
|
100
131
|
if let project = projectStore.activeProject {
|
|
101
132
|
HStack(spacing: 4) {
|
|
102
133
|
Image(systemName: "folder.fill").foregroundStyle(.tint)
|
|
@@ -107,7 +138,7 @@ struct MenuBarPopover: View {
|
|
|
107
138
|
}
|
|
108
139
|
.padding(10)
|
|
109
140
|
.glassEffect(.regular)
|
|
110
|
-
|
|
141
|
+
} else if engine.isTranscribing {
|
|
111
142
|
VStack(spacing: 6) {
|
|
112
143
|
HStack(spacing: 8) {
|
|
113
144
|
ProgressView().controlSize(.small)
|
|
@@ -115,6 +146,15 @@ struct MenuBarPopover: View {
|
|
|
115
146
|
.foregroundStyle(.secondary)
|
|
116
147
|
Spacer()
|
|
117
148
|
}
|
|
149
|
+
// Show accumulated text during transcribing
|
|
150
|
+
if !engine.liveTranscriptionText.isEmpty {
|
|
151
|
+
Text(engine.liveTranscriptionText)
|
|
152
|
+
.font(.callout)
|
|
153
|
+
.foregroundStyle(.primary)
|
|
154
|
+
.multilineTextAlignment(.leading)
|
|
155
|
+
.lineLimit(3)
|
|
156
|
+
.padding(.vertical, 4)
|
|
157
|
+
}
|
|
118
158
|
if let project = projectStore.activeProject {
|
|
119
159
|
HStack(spacing: 4) {
|
|
120
160
|
Image(systemName: "folder.fill").foregroundStyle(.tint)
|
|
@@ -126,7 +166,7 @@ struct MenuBarPopover: View {
|
|
|
126
166
|
.frame(maxWidth: .infinity)
|
|
127
167
|
.padding(10)
|
|
128
168
|
.glassEffect(.regular)
|
|
129
|
-
|
|
169
|
+
} else {
|
|
130
170
|
VStack(spacing: 6) {
|
|
131
171
|
Button { engine.startRecording() } label: {
|
|
132
172
|
Label("Record", systemImage: "mic.circle.fill")
|
|
@@ -137,11 +177,15 @@ struct MenuBarPopover: View {
|
|
|
137
177
|
Text("or hold \(shortcut.description)")
|
|
138
178
|
.foregroundStyle(.tertiary)
|
|
139
179
|
}
|
|
180
|
+
Text(engine.statusMessage)
|
|
181
|
+
.font(.caption)
|
|
182
|
+
.foregroundStyle(engine.statusMessage == "Ready" ? Color.secondary : Color.orange)
|
|
183
|
+
.multilineTextAlignment(.center)
|
|
184
|
+
.lineLimit(2)
|
|
140
185
|
}
|
|
141
186
|
.frame(maxWidth: .infinity)
|
|
142
187
|
.padding(10)
|
|
143
188
|
.glassEffect(.regular)
|
|
144
|
-
}
|
|
145
189
|
}
|
|
146
190
|
}
|
|
147
191
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum NativeAppLog {
|
|
4
|
+
private static let lock = NSLock()
|
|
5
|
+
|
|
6
|
+
static func write(_ message: String, homePath: String = FileManager.default.homeDirectoryForCurrentUser.path) {
|
|
7
|
+
lock.lock()
|
|
8
|
+
defer { lock.unlock() }
|
|
9
|
+
|
|
10
|
+
let dir = "\(homePath)/.hasna/recordings"
|
|
11
|
+
let path = "\(dir)/Recordings.log"
|
|
12
|
+
let line = "[\(Self.timestamp())] \(message)\n"
|
|
13
|
+
|
|
14
|
+
do {
|
|
15
|
+
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
|
|
16
|
+
if FileManager.default.fileExists(atPath: path),
|
|
17
|
+
let handle = FileHandle(forWritingAtPath: path) {
|
|
18
|
+
defer { try? handle.close() }
|
|
19
|
+
try handle.seekToEnd()
|
|
20
|
+
if let data = line.data(using: .utf8) {
|
|
21
|
+
try handle.write(contentsOf: data)
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
try line.write(toFile: path, atomically: true, encoding: .utf8)
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
fputs("[Recordings] log write failed: \(error.localizedDescription)\n", stderr)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private static func timestamp() -> String {
|
|
32
|
+
let formatter = ISO8601DateFormatter()
|
|
33
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
34
|
+
return formatter.string(from: Date())
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
@preconcurrency import AVFoundation
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
enum NativePCMRecorderError: LocalizedError {
|
|
5
|
+
case noInputDevice
|
|
6
|
+
case unsupportedInputFormat
|
|
7
|
+
case failedToCreateConverter
|
|
8
|
+
case failedToStart(String)
|
|
9
|
+
|
|
10
|
+
var errorDescription: String? {
|
|
11
|
+
switch self {
|
|
12
|
+
case .noInputDevice:
|
|
13
|
+
return "No microphone input device is available"
|
|
14
|
+
case .unsupportedInputFormat:
|
|
15
|
+
return "The selected microphone format is not supported"
|
|
16
|
+
case .failedToCreateConverter:
|
|
17
|
+
return "Could not prepare microphone audio conversion"
|
|
18
|
+
case .failedToStart(let message):
|
|
19
|
+
return "Could not start microphone capture: \(message)"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
final class NativePCMRecorder: @unchecked Sendable {
|
|
25
|
+
private let engine = AVAudioEngine()
|
|
26
|
+
private let onPCM: @Sendable (Data) -> Void
|
|
27
|
+
private var converter: AVAudioConverter?
|
|
28
|
+
private var inputFormat: AVAudioFormat?
|
|
29
|
+
private var outputFormat: AVAudioFormat?
|
|
30
|
+
private var started = false
|
|
31
|
+
|
|
32
|
+
init(onPCM: @escaping @Sendable (Data) -> Void) {
|
|
33
|
+
self.onPCM = onPCM
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func start() throws {
|
|
37
|
+
guard !started else { return }
|
|
38
|
+
|
|
39
|
+
let inputNode = engine.inputNode
|
|
40
|
+
let inputFormat = inputNode.inputFormat(forBus: 0)
|
|
41
|
+
guard inputFormat.channelCount > 0 else {
|
|
42
|
+
throw NativePCMRecorderError.noInputDevice
|
|
43
|
+
}
|
|
44
|
+
guard inputFormat.sampleRate > 0 else {
|
|
45
|
+
throw NativePCMRecorderError.unsupportedInputFormat
|
|
46
|
+
}
|
|
47
|
+
guard let outputFormat = Self.realtimeOutputFormat() else {
|
|
48
|
+
throw NativePCMRecorderError.unsupportedInputFormat
|
|
49
|
+
}
|
|
50
|
+
guard let converter = AVAudioConverter(from: inputFormat, to: outputFormat) else {
|
|
51
|
+
throw NativePCMRecorderError.failedToCreateConverter
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
self.converter = converter
|
|
55
|
+
self.inputFormat = inputFormat
|
|
56
|
+
self.outputFormat = outputFormat
|
|
57
|
+
|
|
58
|
+
inputNode.installTap(onBus: 0, bufferSize: 1_024, format: inputFormat) { [weak self] buffer, _ in
|
|
59
|
+
self?.convertAndEmit(buffer)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
do {
|
|
63
|
+
engine.prepare()
|
|
64
|
+
try engine.start()
|
|
65
|
+
started = true
|
|
66
|
+
} catch {
|
|
67
|
+
inputNode.removeTap(onBus: 0)
|
|
68
|
+
self.converter = nil
|
|
69
|
+
self.inputFormat = nil
|
|
70
|
+
self.outputFormat = nil
|
|
71
|
+
throw NativePCMRecorderError.failedToStart(error.localizedDescription)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func stop() {
|
|
76
|
+
guard started else {
|
|
77
|
+
converter = nil
|
|
78
|
+
inputFormat = nil
|
|
79
|
+
outputFormat = nil
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
engine.inputNode.removeTap(onBus: 0)
|
|
84
|
+
engine.stop()
|
|
85
|
+
converter = nil
|
|
86
|
+
inputFormat = nil
|
|
87
|
+
outputFormat = nil
|
|
88
|
+
started = false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
deinit {
|
|
92
|
+
stop()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private func convertAndEmit(_ inputBuffer: AVAudioPCMBuffer) {
|
|
96
|
+
guard let converter, let inputFormat, let outputFormat else { return }
|
|
97
|
+
|
|
98
|
+
let sampleRateRatio = outputFormat.sampleRate / inputFormat.sampleRate
|
|
99
|
+
let estimatedFrames = AVAudioFrameCount(Double(inputBuffer.frameLength) * sampleRateRatio) + 512
|
|
100
|
+
guard let outputBuffer = AVAudioPCMBuffer(
|
|
101
|
+
pcmFormat: outputFormat,
|
|
102
|
+
frameCapacity: max(estimatedFrames, 1)
|
|
103
|
+
) else {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let inputSource = AudioConverterInputSource(buffer: inputBuffer)
|
|
108
|
+
var conversionError: NSError?
|
|
109
|
+
converter.convert(to: outputBuffer, error: &conversionError) { _, status in
|
|
110
|
+
inputSource.next(status: status)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
guard conversionError == nil else { return }
|
|
114
|
+
let data = Self.extractPCM16Data(from: outputBuffer)
|
|
115
|
+
if !data.isEmpty {
|
|
116
|
+
onPCM(data)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static func realtimeOutputFormat() -> AVAudioFormat? {
|
|
121
|
+
AVAudioFormat(
|
|
122
|
+
commonFormat: .pcmFormatInt16,
|
|
123
|
+
sampleRate: 24_000,
|
|
124
|
+
channels: 1,
|
|
125
|
+
interleaved: true
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static func extractPCM16Data(from buffer: AVAudioPCMBuffer) -> Data {
|
|
130
|
+
var data = Data()
|
|
131
|
+
for audioBuffer in UnsafeMutableAudioBufferListPointer(buffer.mutableAudioBufferList) {
|
|
132
|
+
guard let bytes = audioBuffer.mData?.assumingMemoryBound(to: UInt8.self),
|
|
133
|
+
audioBuffer.mDataByteSize > 0 else {
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
data.append(bytes, count: Int(audioBuffer.mDataByteSize))
|
|
137
|
+
}
|
|
138
|
+
return data
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private final class AudioConverterInputSource: @unchecked Sendable {
|
|
143
|
+
private let buffer: AVAudioPCMBuffer
|
|
144
|
+
private let lock = NSLock()
|
|
145
|
+
private var consumed = false
|
|
146
|
+
|
|
147
|
+
init(buffer: AVAudioPCMBuffer) {
|
|
148
|
+
self.buffer = buffer
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func next(status: UnsafeMutablePointer<AVAudioConverterInputStatus>) -> AVAudioBuffer? {
|
|
152
|
+
lock.lock()
|
|
153
|
+
defer { lock.unlock() }
|
|
154
|
+
|
|
155
|
+
if consumed {
|
|
156
|
+
status.pointee = .noDataNow
|
|
157
|
+
return nil
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
consumed = true
|
|
161
|
+
status.pointee = .haveData
|
|
162
|
+
return buffer
|
|
163
|
+
}
|
|
164
|
+
}
|