@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/src/index.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
// ── Types ───────────────────────────────────────────────────────────────────
|
|
2
|
-
export type {
|
|
3
|
-
Recording,
|
|
4
|
-
CreateRecordingInput,
|
|
5
|
-
RecordingFilter,
|
|
6
|
-
ProcessingMode,
|
|
7
|
-
Agent,
|
|
8
|
-
Project,
|
|
9
|
-
RecordingsConfig,
|
|
10
|
-
TranscriptionResult,
|
|
11
|
-
EnhancementResult,
|
|
12
|
-
} from "./types/index.js";
|
|
13
|
-
|
|
14
|
-
export {
|
|
15
|
-
RecordingNotFoundError,
|
|
16
|
-
RecordingError,
|
|
17
|
-
TranscriptionError,
|
|
18
|
-
EnhancementError,
|
|
19
|
-
} from "./types/index.js";
|
|
20
|
-
|
|
21
|
-
// ── Database ────────────────────────────────────────────────────────────────
|
|
22
|
-
export {
|
|
23
|
-
getDatabase,
|
|
24
|
-
closeDatabase,
|
|
25
|
-
resetDatabase,
|
|
26
|
-
getDbPath,
|
|
27
|
-
shortUuid,
|
|
28
|
-
} from "./db/database.js";
|
|
29
|
-
|
|
30
|
-
// ── Recordings CRUD ─────────────────────────────────────────────────────────
|
|
31
|
-
export {
|
|
32
|
-
createRecording,
|
|
33
|
-
getRecording,
|
|
34
|
-
listRecordings,
|
|
35
|
-
deleteRecording,
|
|
36
|
-
searchRecordings,
|
|
37
|
-
getRecordingStats,
|
|
38
|
-
} from "./db/recordings.js";
|
|
39
|
-
|
|
40
|
-
// ── Agents ──────────────────────────────────────────────────────────────────
|
|
41
|
-
export { registerAgent, getAgent, listAgents } from "./db/agents.js";
|
|
42
|
-
|
|
43
|
-
// ── Projects ────────────────────────────────────────────────────────────────
|
|
44
|
-
export {
|
|
45
|
-
registerProject,
|
|
46
|
-
getProject,
|
|
47
|
-
listProjects,
|
|
48
|
-
} from "./db/projects.js";
|
|
49
|
-
|
|
50
|
-
// ── Config ──────────────────────────────────────────────────────────────────
|
|
51
|
-
export {
|
|
52
|
-
loadConfig,
|
|
53
|
-
getDataDir,
|
|
54
|
-
ensureDataDir,
|
|
55
|
-
DEFAULT_CONFIG,
|
|
56
|
-
} from "./lib/config.js";
|
|
57
|
-
|
|
58
|
-
// ── Transcription ───────────────────────────────────────────────────────────
|
|
59
|
-
export {
|
|
60
|
-
transcribeAudio,
|
|
61
|
-
transcribeBuffer,
|
|
62
|
-
resetClient,
|
|
63
|
-
} from "./lib/transcriber.js";
|
|
64
|
-
|
|
65
|
-
// ── Enhancement ─────────────────────────────────────────────────────────────
|
|
66
|
-
export {
|
|
67
|
-
needsEnhancement,
|
|
68
|
-
enhanceText,
|
|
69
|
-
processText,
|
|
70
|
-
resetEnhancementClient,
|
|
71
|
-
} from "./lib/enhancer.js";
|
|
72
|
-
|
|
73
|
-
// ── Recorder ────────────────────────────────────────────────────────────────
|
|
74
|
-
export {
|
|
75
|
-
startRecording,
|
|
76
|
-
stopRecording,
|
|
77
|
-
isRecording,
|
|
78
|
-
getCurrentFile,
|
|
79
|
-
checkRecordingDeps,
|
|
80
|
-
recordDuration,
|
|
81
|
-
} from "./lib/recorder.js";
|
package/src/lib/config.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, mkdirSync, cpSync, readdirSync, statSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import type { RecordingsConfig } from "../types/index.js";
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_CONFIG: RecordingsConfig = {
|
|
7
|
-
openai_api_key: "",
|
|
8
|
-
enhancement_api_key: "",
|
|
9
|
-
transcription_model: "gpt-4o-mini-transcribe",
|
|
10
|
-
enhancement_model: "gpt-4o",
|
|
11
|
-
language: "en",
|
|
12
|
-
audio_format: "wav",
|
|
13
|
-
sample_rate: 16000,
|
|
14
|
-
record_command: "sox",
|
|
15
|
-
hotkey: "space",
|
|
16
|
-
auto_enhance: true,
|
|
17
|
-
enhance_triggers: [
|
|
18
|
-
"say it better",
|
|
19
|
-
"rewrite this",
|
|
20
|
-
"make it sound",
|
|
21
|
-
"clean this up",
|
|
22
|
-
"fix this",
|
|
23
|
-
"rephrase",
|
|
24
|
-
"write it properly",
|
|
25
|
-
"make it professional",
|
|
26
|
-
"improve this",
|
|
27
|
-
"polish this",
|
|
28
|
-
],
|
|
29
|
-
db_path: "",
|
|
30
|
-
audio_dir: "",
|
|
31
|
-
max_recording_seconds: 300,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export function loadConfig(configPath?: string): RecordingsConfig {
|
|
35
|
-
const config = { ...DEFAULT_CONFIG };
|
|
36
|
-
|
|
37
|
-
// 1. Load from config file
|
|
38
|
-
const filePath =
|
|
39
|
-
configPath || findConfigFile() || join(getDataDir(), "config.json");
|
|
40
|
-
|
|
41
|
-
if (existsSync(filePath)) {
|
|
42
|
-
try {
|
|
43
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
44
|
-
const fileConfig = JSON.parse(raw) as Partial<RecordingsConfig>;
|
|
45
|
-
Object.assign(config, expandEnvBackedConfig(fileConfig));
|
|
46
|
-
} catch {
|
|
47
|
-
// Ignore invalid config files
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 2. Override with env vars
|
|
52
|
-
if (process.env.OPENAI_API_KEY) {
|
|
53
|
-
config.openai_api_key = process.env.OPENAI_API_KEY;
|
|
54
|
-
}
|
|
55
|
-
if (process.env.RECORDINGS_API_KEY) {
|
|
56
|
-
config.openai_api_key = process.env.RECORDINGS_API_KEY;
|
|
57
|
-
}
|
|
58
|
-
if (process.env.RECORDINGS_ENHANCEMENT_KEY) {
|
|
59
|
-
config.enhancement_api_key = process.env.RECORDINGS_ENHANCEMENT_KEY;
|
|
60
|
-
}
|
|
61
|
-
if (process.env.RECORDINGS_MODEL) {
|
|
62
|
-
config.transcription_model = process.env.RECORDINGS_MODEL;
|
|
63
|
-
}
|
|
64
|
-
if (process.env.RECORDINGS_ENHANCEMENT_MODEL) {
|
|
65
|
-
config.enhancement_model = process.env.RECORDINGS_ENHANCEMENT_MODEL;
|
|
66
|
-
}
|
|
67
|
-
if (process.env.RECORDINGS_LANGUAGE) {
|
|
68
|
-
config.language = process.env.RECORDINGS_LANGUAGE;
|
|
69
|
-
}
|
|
70
|
-
if (process.env.HASNA_RECORDINGS_DB_PATH) {
|
|
71
|
-
config.db_path = process.env.HASNA_RECORDINGS_DB_PATH;
|
|
72
|
-
} else if (process.env.RECORDINGS_DB_PATH) {
|
|
73
|
-
config.db_path = process.env.RECORDINGS_DB_PATH;
|
|
74
|
-
}
|
|
75
|
-
if (process.env.RECORDINGS_AUDIO_DIR) {
|
|
76
|
-
config.audio_dir = process.env.RECORDINGS_AUDIO_DIR;
|
|
77
|
-
}
|
|
78
|
-
if (process.env.RECORDINGS_MAX_SECONDS) {
|
|
79
|
-
config.max_recording_seconds = parseInt(
|
|
80
|
-
process.env.RECORDINGS_MAX_SECONDS,
|
|
81
|
-
10
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 3. Load API key from ~/.secrets if not set
|
|
86
|
-
if (!config.openai_api_key) {
|
|
87
|
-
config.openai_api_key = loadSecretKey("OPENAI_API_KEY");
|
|
88
|
-
}
|
|
89
|
-
if (!config.enhancement_api_key) {
|
|
90
|
-
config.enhancement_api_key =
|
|
91
|
-
config.openai_api_key || loadSecretKey("OPENAI_API_KEY");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 4. Set defaults for paths
|
|
95
|
-
if (!config.db_path) {
|
|
96
|
-
config.db_path = join(getDataDir(), "recordings.db");
|
|
97
|
-
}
|
|
98
|
-
if (!config.audio_dir) {
|
|
99
|
-
config.audio_dir = join(getDataDir(), "audio");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return config;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function expandEnvBackedConfig(config: Partial<RecordingsConfig>): Partial<RecordingsConfig> {
|
|
106
|
-
const expanded = { ...config };
|
|
107
|
-
|
|
108
|
-
for (const key of ["openai_api_key", "enhancement_api_key"] as const) {
|
|
109
|
-
const value = expanded[key];
|
|
110
|
-
if (typeof value === "string" && value.startsWith("$") && value.length > 1) {
|
|
111
|
-
expanded[key] = process.env[value.slice(1)] || value;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return expanded;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function findConfigFile(): string | null {
|
|
119
|
-
// Walk up from cwd looking for .recordings/config.json
|
|
120
|
-
let dir = process.cwd();
|
|
121
|
-
const root = "/";
|
|
122
|
-
while (dir !== root) {
|
|
123
|
-
const candidate = join(dir, ".recordings", "config.json");
|
|
124
|
-
if (existsSync(candidate)) return candidate;
|
|
125
|
-
const parent = join(dir, "..");
|
|
126
|
-
if (parent === dir) break;
|
|
127
|
-
dir = parent;
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function getDataDir(): string {
|
|
133
|
-
// Check for .recordings in cwd hierarchy (project-local)
|
|
134
|
-
let dir = process.cwd();
|
|
135
|
-
const root = "/";
|
|
136
|
-
while (dir !== root) {
|
|
137
|
-
const candidate = join(dir, ".recordings");
|
|
138
|
-
if (existsSync(candidate)) return candidate;
|
|
139
|
-
const parent = join(dir, "..");
|
|
140
|
-
if (parent === dir) break;
|
|
141
|
-
dir = parent;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Global: ~/.hasna/recordings (with backward compat from ~/.recordings)
|
|
145
|
-
const home = homedir();
|
|
146
|
-
const newDir = join(home, ".hasna", "recordings");
|
|
147
|
-
const oldDir = join(home, ".recordings");
|
|
148
|
-
|
|
149
|
-
// Auto-migrate from old location if new dir doesn't exist yet
|
|
150
|
-
if (!existsSync(newDir) && existsSync(oldDir)) {
|
|
151
|
-
try {
|
|
152
|
-
mkdirSync(join(home, ".hasna"), { recursive: true });
|
|
153
|
-
cpSync(oldDir, newDir, { recursive: true });
|
|
154
|
-
} catch {
|
|
155
|
-
// Fall through to use new dir
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return newDir;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function loadSecretKey(keyName: string): string {
|
|
163
|
-
const secretsPath = join(homedir(), ".secrets");
|
|
164
|
-
if (!existsSync(secretsPath)) return "";
|
|
165
|
-
|
|
166
|
-
for (const candidate of listSecretFiles(secretsPath)) {
|
|
167
|
-
try {
|
|
168
|
-
const content = readFileSync(candidate, "utf-8");
|
|
169
|
-
const match = content.match(
|
|
170
|
-
new RegExp(`export\\s+${keyName}\\s*=\\s*"([^"]+)"`)
|
|
171
|
-
);
|
|
172
|
-
if (match) return match[1]!;
|
|
173
|
-
|
|
174
|
-
const match2 = content.match(
|
|
175
|
-
new RegExp(`export\\s+${keyName}\\s*=\\s*'([^']+)'`)
|
|
176
|
-
);
|
|
177
|
-
if (match2) return match2[1]!;
|
|
178
|
-
|
|
179
|
-
const match3 = content.match(new RegExp(`${keyName}\\s*=\\s*(.+)`));
|
|
180
|
-
if (match3) return match3[1]!.trim().replace(/^["']|["']$/g, "");
|
|
181
|
-
} catch {
|
|
182
|
-
// Ignore unreadable secret files
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return "";
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function listSecretFiles(path: string): string[] {
|
|
190
|
-
try {
|
|
191
|
-
const stats = statSync(path);
|
|
192
|
-
if (stats.isFile()) return [path];
|
|
193
|
-
if (!stats.isDirectory()) return [];
|
|
194
|
-
|
|
195
|
-
return readdirSync(path)
|
|
196
|
-
.sort()
|
|
197
|
-
.flatMap((entry) => {
|
|
198
|
-
const child = join(path, entry);
|
|
199
|
-
try {
|
|
200
|
-
const childStats = statSync(child);
|
|
201
|
-
if (childStats.isDirectory()) return listSecretFiles(child);
|
|
202
|
-
if (childStats.isFile() && child.endsWith(".env")) return [child];
|
|
203
|
-
} catch {
|
|
204
|
-
// Ignore entries that disappear or are unreadable
|
|
205
|
-
}
|
|
206
|
-
return [];
|
|
207
|
-
});
|
|
208
|
-
} catch {
|
|
209
|
-
return [];
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function ensureDataDir(config: RecordingsConfig): void {
|
|
214
|
-
const { mkdirSync } = require("fs") as typeof import("fs");
|
|
215
|
-
mkdirSync(config.audio_dir, { recursive: true });
|
|
216
|
-
|
|
217
|
-
// Ensure db directory exists
|
|
218
|
-
const dbDir = config.db_path.substring(
|
|
219
|
-
0,
|
|
220
|
-
config.db_path.lastIndexOf("/")
|
|
221
|
-
);
|
|
222
|
-
if (dbDir) mkdirSync(dbDir, { recursive: true });
|
|
223
|
-
}
|
package/src/lib/enhancer.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import OpenAI from "openai";
|
|
2
|
-
import type {
|
|
3
|
-
RecordingsConfig,
|
|
4
|
-
EnhancementResult,
|
|
5
|
-
} from "../types/index.js";
|
|
6
|
-
import { EnhancementError } from "../types/index.js";
|
|
7
|
-
|
|
8
|
-
let _enhancementClient: OpenAI | null = null;
|
|
9
|
-
|
|
10
|
-
function getEnhancementClient(config: RecordingsConfig): OpenAI {
|
|
11
|
-
if (_enhancementClient) return _enhancementClient;
|
|
12
|
-
const key = config.enhancement_api_key || config.openai_api_key;
|
|
13
|
-
if (!key) {
|
|
14
|
-
throw new EnhancementError(
|
|
15
|
-
"API key not configured for enhancement. Set OPENAI_API_KEY or RECORDINGS_ENHANCEMENT_KEY"
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
_enhancementClient = new OpenAI({ apiKey: key });
|
|
19
|
-
return _enhancementClient;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function resetEnhancementClient(): void {
|
|
23
|
-
_enhancementClient = null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Detect if the transcribed text needs AI enhancement.
|
|
28
|
-
*
|
|
29
|
-
* Detection logic:
|
|
30
|
-
* 1. Explicit triggers: "say it better", "rewrite this", etc.
|
|
31
|
-
* 2. Instruction patterns: "write an email saying...", "give instructions to..."
|
|
32
|
-
* 3. Meta-commentary: text that talks ABOUT what to say rather than being the content itself
|
|
33
|
-
*/
|
|
34
|
-
export function needsEnhancement(
|
|
35
|
-
text: string,
|
|
36
|
-
config: RecordingsConfig
|
|
37
|
-
): { needs: boolean; reason: string; instruction: string } {
|
|
38
|
-
const lower = text.toLowerCase().trim();
|
|
39
|
-
|
|
40
|
-
// Check explicit triggers from config
|
|
41
|
-
for (const trigger of config.enhance_triggers) {
|
|
42
|
-
if (lower.includes(trigger.toLowerCase())) {
|
|
43
|
-
return {
|
|
44
|
-
needs: true,
|
|
45
|
-
reason: `Explicit trigger: "${trigger}"`,
|
|
46
|
-
instruction: extractInstruction(text, trigger),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Check instruction patterns
|
|
52
|
-
const instructionPatterns = [
|
|
53
|
-
/(?:write|draft|compose|create)\s+(?:an?\s+)?(?:email|message|response|reply|letter|note|text|slack|dm)/i,
|
|
54
|
-
/(?:give|provide|send)\s+(?:them|him|her|it|the\s+agent|the\s+team)\s+(?:full\s+)?instructions/i,
|
|
55
|
-
/(?:tell|ask)\s+(?:them|him|her|it|the\s+agent)\s+(?:to|that)/i,
|
|
56
|
-
/(?:make\s+it|make\s+this)\s+(?:sound|look|read)\s+(?:more\s+)?(?:professional|formal|casual|friendly|better)/i,
|
|
57
|
-
/(?:ok\s+so|okay\s+so|alright\s+so)\s+(?:say|write|tell|put)/i,
|
|
58
|
-
/(?:i\s+need|i\s+want)\s+(?:the\s+agent|it|them|you)\s+to\s+(?:build|create|implement|design|make)/i,
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
for (const pattern of instructionPatterns) {
|
|
62
|
-
if (pattern.test(text)) {
|
|
63
|
-
return {
|
|
64
|
-
needs: true,
|
|
65
|
-
reason: `Instruction pattern detected`,
|
|
66
|
-
instruction: text,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return { needs: false, reason: "Direct dictation", instruction: text };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function extractInstruction(text: string, trigger: string): string {
|
|
75
|
-
const lower = text.toLowerCase();
|
|
76
|
-
const idx = lower.indexOf(trigger.toLowerCase());
|
|
77
|
-
if (idx === -1) return text;
|
|
78
|
-
|
|
79
|
-
// Take everything after the trigger as the instruction context
|
|
80
|
-
const after = text.substring(idx + trigger.length).trim();
|
|
81
|
-
const before = text.substring(0, idx).trim();
|
|
82
|
-
|
|
83
|
-
// If there's content before the trigger, that's likely the raw text to enhance
|
|
84
|
-
if (before.length > after.length && before.length > 10) {
|
|
85
|
-
return before;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Otherwise the whole text is the instruction
|
|
89
|
-
return text;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export async function enhanceText(
|
|
93
|
-
rawText: string,
|
|
94
|
-
instruction: string,
|
|
95
|
-
config: RecordingsConfig,
|
|
96
|
-
systemPrompt?: string
|
|
97
|
-
): Promise<EnhancementResult> {
|
|
98
|
-
const client = getEnhancementClient(config);
|
|
99
|
-
|
|
100
|
-
const basePrompt = `You are a writing assistant. The user has dictated speech that needs to be transformed into polished output.
|
|
101
|
-
|
|
102
|
-
Rules:
|
|
103
|
-
- Output ONLY the enhanced/rewritten text — no explanations, no preamble
|
|
104
|
-
- Preserve the user's intent and meaning
|
|
105
|
-
- Fix grammar, structure, and clarity
|
|
106
|
-
- If the user is giving instructions (e.g., "write an email saying..."), produce the actual output (the email), not a description of it
|
|
107
|
-
- If the user says "say it better" or similar, rewrite their preceding text to be clearer and more professional
|
|
108
|
-
- Match the appropriate tone (formal for business, casual for personal)`;
|
|
109
|
-
|
|
110
|
-
const fullPrompt = systemPrompt ? `${basePrompt}\n\nAdditional context:\n${systemPrompt}` : basePrompt;
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const response = await client.chat.completions.create({
|
|
114
|
-
model: config.enhancement_model,
|
|
115
|
-
messages: [
|
|
116
|
-
{
|
|
117
|
-
role: "system",
|
|
118
|
-
content: fullPrompt,
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
role: "user",
|
|
122
|
-
content: instruction,
|
|
123
|
-
},
|
|
124
|
-
],
|
|
125
|
-
temperature: 0.3,
|
|
126
|
-
max_tokens: 4096,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
const enhanced =
|
|
130
|
-
response.choices[0]?.message?.content?.trim() || rawText;
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
original: rawText,
|
|
134
|
-
enhanced,
|
|
135
|
-
model: config.enhancement_model,
|
|
136
|
-
reasoning: null,
|
|
137
|
-
};
|
|
138
|
-
} catch (error) {
|
|
139
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
140
|
-
throw new EnhancementError(`Enhancement failed: ${msg}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Full pipeline: detect if enhancement is needed, enhance if so.
|
|
146
|
-
*/
|
|
147
|
-
export async function processText(
|
|
148
|
-
rawText: string,
|
|
149
|
-
config: RecordingsConfig,
|
|
150
|
-
systemPrompt?: string
|
|
151
|
-
): Promise<{
|
|
152
|
-
text: string;
|
|
153
|
-
mode: "raw" | "enhanced";
|
|
154
|
-
enhancement_model: string | null;
|
|
155
|
-
}> {
|
|
156
|
-
if (!config.auto_enhance) {
|
|
157
|
-
return { text: rawText, mode: "raw", enhancement_model: null };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const detection = needsEnhancement(rawText, config);
|
|
161
|
-
|
|
162
|
-
if (!detection.needs) {
|
|
163
|
-
return { text: rawText, mode: "raw", enhancement_model: null };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const result = await enhanceText(rawText, detection.instruction, config, systemPrompt);
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
text: result.enhanced,
|
|
170
|
-
mode: "enhanced",
|
|
171
|
-
enhancement_model: result.model,
|
|
172
|
-
};
|
|
173
|
-
}
|
package/src/lib/recorder.ts
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from "child_process";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { existsSync } from "fs";
|
|
4
|
-
import type { RecordingsConfig } from "../types/index.js";
|
|
5
|
-
import { RecordingError } from "../types/index.js";
|
|
6
|
-
|
|
7
|
-
let _recordProcess: ChildProcess | null = null;
|
|
8
|
-
let _currentFile: string | null = null;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Check if sox/rec is available for recording
|
|
12
|
-
*/
|
|
13
|
-
export async function checkRecordingDeps(): Promise<{
|
|
14
|
-
available: boolean;
|
|
15
|
-
tool: string;
|
|
16
|
-
message: string;
|
|
17
|
-
}> {
|
|
18
|
-
// Check for sox
|
|
19
|
-
try {
|
|
20
|
-
const proc = Bun.spawn(["which", "sox"], {
|
|
21
|
-
stdout: "pipe",
|
|
22
|
-
stderr: "pipe",
|
|
23
|
-
});
|
|
24
|
-
await proc.exited;
|
|
25
|
-
if (proc.exitCode === 0) {
|
|
26
|
-
return { available: true, tool: "sox", message: "sox is available" };
|
|
27
|
-
}
|
|
28
|
-
} catch {
|
|
29
|
-
// Not available
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Check for rec (part of sox)
|
|
33
|
-
try {
|
|
34
|
-
const proc = Bun.spawn(["which", "rec"], {
|
|
35
|
-
stdout: "pipe",
|
|
36
|
-
stderr: "pipe",
|
|
37
|
-
});
|
|
38
|
-
await proc.exited;
|
|
39
|
-
if (proc.exitCode === 0) {
|
|
40
|
-
return { available: true, tool: "rec", message: "rec is available" };
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
// Not available
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Check for ffmpeg
|
|
47
|
-
try {
|
|
48
|
-
const proc = Bun.spawn(["which", "ffmpeg"], {
|
|
49
|
-
stdout: "pipe",
|
|
50
|
-
stderr: "pipe",
|
|
51
|
-
});
|
|
52
|
-
await proc.exited;
|
|
53
|
-
if (proc.exitCode === 0) {
|
|
54
|
-
return {
|
|
55
|
-
available: true,
|
|
56
|
-
tool: "ffmpeg",
|
|
57
|
-
message: "ffmpeg is available",
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// Not available
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
available: false,
|
|
66
|
-
tool: "none",
|
|
67
|
-
message:
|
|
68
|
-
"No recording tool found. Install sox: brew install sox (macOS) or apt install sox (Linux)",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Start recording audio from microphone
|
|
74
|
-
*/
|
|
75
|
-
export function startRecording(config: RecordingsConfig): string {
|
|
76
|
-
if (_recordProcess) {
|
|
77
|
-
throw new RecordingError("Already recording. Stop the current recording first.");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
81
|
-
const filename = `recording-${timestamp}.${config.audio_format}`;
|
|
82
|
-
const filepath = join(config.audio_dir, filename);
|
|
83
|
-
|
|
84
|
-
// Build sox/rec command based on config
|
|
85
|
-
const args = buildRecordArgs(filepath, config);
|
|
86
|
-
|
|
87
|
-
_recordProcess = spawn(args[0]!, args.slice(1), {
|
|
88
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
_currentFile = filepath;
|
|
92
|
-
|
|
93
|
-
_recordProcess.on("error", (err) => {
|
|
94
|
-
_recordProcess = null;
|
|
95
|
-
_currentFile = null;
|
|
96
|
-
throw new RecordingError(`Recording process error: ${err.message}`);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
_recordProcess.on("exit", () => {
|
|
100
|
-
_recordProcess = null;
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
return filepath;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Stop the current recording
|
|
108
|
-
*/
|
|
109
|
-
export function stopRecording(): string | null {
|
|
110
|
-
if (!_recordProcess) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const filepath = _currentFile;
|
|
115
|
-
|
|
116
|
-
// Send SIGINT to gracefully stop sox/rec
|
|
117
|
-
_recordProcess.kill("SIGINT");
|
|
118
|
-
_recordProcess = null;
|
|
119
|
-
_currentFile = null;
|
|
120
|
-
|
|
121
|
-
return filepath;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Check if currently recording
|
|
126
|
-
*/
|
|
127
|
-
export function isRecording(): boolean {
|
|
128
|
-
return _recordProcess !== null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Get current recording file path
|
|
133
|
-
*/
|
|
134
|
-
export function getCurrentFile(): string | null {
|
|
135
|
-
return _currentFile;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function buildRecordArgs(filepath: string, config: RecordingsConfig): string[] {
|
|
139
|
-
const format = config.audio_format;
|
|
140
|
-
const rate = config.sample_rate;
|
|
141
|
-
const maxSeconds = config.max_recording_seconds;
|
|
142
|
-
|
|
143
|
-
// Use rec (sox) for recording — most reliable cross-platform
|
|
144
|
-
// rec outputs to file, auto-detects input device
|
|
145
|
-
return [
|
|
146
|
-
"rec",
|
|
147
|
-
"-r",
|
|
148
|
-
rate.toString(),
|
|
149
|
-
"-c",
|
|
150
|
-
"1", // mono
|
|
151
|
-
"-b",
|
|
152
|
-
"16", // 16-bit
|
|
153
|
-
filepath,
|
|
154
|
-
"trim",
|
|
155
|
-
"0",
|
|
156
|
-
maxSeconds.toString(),
|
|
157
|
-
];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Record for a specific duration (non-interactive)
|
|
162
|
-
*/
|
|
163
|
-
export async function recordDuration(
|
|
164
|
-
seconds: number,
|
|
165
|
-
config: RecordingsConfig
|
|
166
|
-
): Promise<string> {
|
|
167
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
168
|
-
const filename = `recording-${timestamp}.${config.audio_format}`;
|
|
169
|
-
const filepath = join(config.audio_dir, filename);
|
|
170
|
-
|
|
171
|
-
const args = [
|
|
172
|
-
"rec",
|
|
173
|
-
"-r",
|
|
174
|
-
config.sample_rate.toString(),
|
|
175
|
-
"-c",
|
|
176
|
-
"1",
|
|
177
|
-
"-b",
|
|
178
|
-
"16",
|
|
179
|
-
filepath,
|
|
180
|
-
"trim",
|
|
181
|
-
"0",
|
|
182
|
-
seconds.toString(),
|
|
183
|
-
];
|
|
184
|
-
|
|
185
|
-
const proc = Bun.spawn(args, {
|
|
186
|
-
stdout: "pipe",
|
|
187
|
-
stderr: "pipe",
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const exitCode = await proc.exited;
|
|
191
|
-
|
|
192
|
-
if (exitCode !== 0 && !existsSync(filepath)) {
|
|
193
|
-
const stderr = await new Response(proc.stderr).text();
|
|
194
|
-
throw new RecordingError(`Recording failed (exit ${exitCode}): ${stderr}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return filepath;
|
|
198
|
-
}
|