@eldrforge/kodrdriv 0.0.14 → 0.0.17
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 +1 -9
- package/dist/arguments.js +306 -55
- package/dist/arguments.js.map +1 -1
- package/dist/audio/devices.js +284 -0
- package/dist/audio/devices.js.map +1 -0
- package/dist/audio/index.js +31 -0
- package/dist/audio/index.js.map +1 -0
- package/dist/audio/processor.js +766 -0
- package/dist/audio/processor.js.map +1 -0
- package/dist/audio/types.js +16 -0
- package/dist/audio/types.js.map +1 -0
- package/dist/audio/validation.js +35 -0
- package/dist/audio/validation.js.map +1 -0
- package/dist/commands/audio-commit.js +64 -248
- package/dist/commands/audio-commit.js.map +1 -1
- package/dist/commands/audio-review.js +90 -701
- package/dist/commands/audio-review.js.map +1 -1
- package/dist/commands/commit.js +18 -18
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/release.js +14 -15
- package/dist/commands/release.js.map +1 -1
- package/dist/commands/review.js +152 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/select-audio.js +265 -0
- package/dist/commands/select-audio.js.map +1 -0
- package/dist/constants.js +86 -68
- package/dist/constants.js.map +1 -1
- package/dist/content/diff.js +155 -1
- package/dist/content/diff.js.map +1 -1
- package/dist/content/issues.js +256 -0
- package/dist/content/issues.js.map +1 -0
- package/dist/content/releaseNotes.js +90 -0
- package/dist/content/releaseNotes.js.map +1 -0
- package/dist/main.js +9 -2
- package/dist/main.js.map +1 -1
- package/dist/prompt/instructions/commit.md +18 -15
- package/dist/prompt/instructions/release.md +6 -5
- package/dist/prompt/instructions/{audio-review.md → review.md} +24 -18
- package/dist/prompt/prompts.js +87 -19
- package/dist/prompt/prompts.js.map +1 -1
- package/dist/types.js +27 -3
- package/dist/types.js.map +1 -1
- package/dist/util/general.js +7 -1
- package/dist/util/general.js.map +1 -1
- package/dist/util/openai.js +34 -3
- package/dist/util/openai.js.map +1 -1
- package/dist/util/stdin.js +61 -0
- package/dist/util/stdin.js.map +1 -0
- package/package.json +6 -6
- package/.kodrdriv/config.yaml +0 -20
- package/.kodrdriv/context/content.md +0 -7
- package/RELEASE_NOTES.md +0 -14
- package/docs/index.html +0 -17
- package/docs/package.json +0 -36
- package/docs/pnpm-lock.yaml +0 -3441
- package/docs/public/README.md +0 -132
- package/docs/public/advanced-usage.md +0 -188
- package/docs/public/code-icon.svg +0 -4
- package/docs/public/commands.md +0 -116
- package/docs/public/configuration.md +0 -274
- package/docs/public/examples.md +0 -352
- package/docs/public/kodrdriv-logo.svg +0 -62
- package/docs/src/App.css +0 -387
- package/docs/src/App.tsx +0 -60
- package/docs/src/components/DocumentPage.tsx +0 -56
- package/docs/src/components/ErrorMessage.tsx +0 -15
- package/docs/src/components/LoadingSpinner.tsx +0 -14
- package/docs/src/components/MarkdownRenderer.tsx +0 -56
- package/docs/src/components/Navigation.css +0 -73
- package/docs/src/components/Navigation.tsx +0 -36
- package/docs/src/index.css +0 -61
- package/docs/src/main.tsx +0 -10
- package/docs/src/test/setup.ts +0 -1
- package/docs/src/vite-env.d.ts +0 -10
- package/docs/tsconfig.node.json +0 -13
- package/docs/vite.config.ts +0 -15
- package/docs/vitest.config.ts +0 -15
- package/eslint.config.mjs +0 -83
- package/nodemon.json +0 -14
- package/output/kodrdriv/250701-1442-release-notes.md +0 -3
- package/pnpm-workspace.yaml +0 -5
- package/tsconfig.tsbuildinfo +0 -1
- package/vite.config.ts +0 -90
- package/vitest.config.ts +0 -24
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { DEFAULT_OUTPUT_DIRECTORY } from '../constants.js';
|
|
4
|
+
import { getLogger } from '../logging.js';
|
|
5
|
+
import { run } from '../util/child.js';
|
|
6
|
+
import { getTimestampedTranscriptFilename, getOutputPath, getTimestampedAudioFilename } from '../util/general.js';
|
|
7
|
+
import { transcribeAudio } from '../util/openai.js';
|
|
8
|
+
import { create } from '../util/storage.js';
|
|
9
|
+
import { audioDeviceConfigExists, loadAudioDeviceFromHomeConfig } from '../commands/select-audio.js';
|
|
10
|
+
import { listAudioDevices, detectBestAudioDevice } from './devices.js';
|
|
11
|
+
import { validateAudioFile } from './validation.js';
|
|
12
|
+
|
|
13
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */ function _define_property(obj, key, value) {
|
|
14
|
+
if (key in obj) {
|
|
15
|
+
Object.defineProperty(obj, key, {
|
|
16
|
+
value: value,
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true
|
|
20
|
+
});
|
|
21
|
+
} else {
|
|
22
|
+
obj[key] = value;
|
|
23
|
+
}
|
|
24
|
+
return obj;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Main audio processor class that handles recording, processing, and transcription
|
|
28
|
+
*/ class AudioProcessor {
|
|
29
|
+
/**
|
|
30
|
+
* Process audio from either a file or by recording new audio
|
|
31
|
+
* @param options Audio processing options
|
|
32
|
+
* @returns AudioProcessingResult with transcript and file paths
|
|
33
|
+
*/ async processAudio(options) {
|
|
34
|
+
// Check if audio device is configured (only for recording, not for file processing)
|
|
35
|
+
if (!options.file && options.preferencesDirectory && !await audioDeviceConfigExists(options.preferencesDirectory)) {
|
|
36
|
+
throw new Error('No audio device configured. Please run "kodrdriv select-audio" first to configure your audio device.');
|
|
37
|
+
}
|
|
38
|
+
if (options.dryRun) {
|
|
39
|
+
if (options.file) {
|
|
40
|
+
this.logger.info('DRY RUN: Would process audio file: %s', options.file);
|
|
41
|
+
} else {
|
|
42
|
+
this.logger.info('DRY RUN: Would start audio recording');
|
|
43
|
+
}
|
|
44
|
+
this.logger.info('DRY RUN: Would transcribe audio and return transcript');
|
|
45
|
+
return {
|
|
46
|
+
transcript: '',
|
|
47
|
+
cancelled: false
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (options.file) {
|
|
51
|
+
// Process existing audio file
|
|
52
|
+
return await this.processAudioFile(options.file, options);
|
|
53
|
+
} else {
|
|
54
|
+
// Record new audio
|
|
55
|
+
return await this.recordAndTranscribeAudio(options);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Process an existing audio file
|
|
60
|
+
*/ async processAudioFile(filePath, options) {
|
|
61
|
+
this.logger.info('🎯 Processing audio file: %s', filePath);
|
|
62
|
+
// Validate the audio file
|
|
63
|
+
await validateAudioFile(filePath);
|
|
64
|
+
// Transcribe the audio
|
|
65
|
+
this.logger.info('🎯 Transcribing audio...');
|
|
66
|
+
this.logger.info('⏳ This may take a few seconds depending on audio length...');
|
|
67
|
+
const transcription = await transcribeAudio(filePath);
|
|
68
|
+
const audioContext = transcription.text;
|
|
69
|
+
this.logger.info('✅ Audio transcribed successfully');
|
|
70
|
+
this.logger.debug('Transcription: %s', audioContext);
|
|
71
|
+
// Save transcript to output directory
|
|
72
|
+
let transcriptFilePath;
|
|
73
|
+
if (options.outputDirectory) {
|
|
74
|
+
transcriptFilePath = await this.saveTranscript(audioContext, filePath, options.outputDirectory);
|
|
75
|
+
}
|
|
76
|
+
if (!audioContext.trim()) {
|
|
77
|
+
this.logger.warn('No audio content was transcribed.');
|
|
78
|
+
return {
|
|
79
|
+
transcript: '',
|
|
80
|
+
audioFilePath: filePath,
|
|
81
|
+
transcriptFilePath,
|
|
82
|
+
cancelled: false
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
this.logger.info('📝 Audio transcribed successfully');
|
|
86
|
+
return {
|
|
87
|
+
transcript: audioContext,
|
|
88
|
+
audioFilePath: filePath,
|
|
89
|
+
transcriptFilePath,
|
|
90
|
+
cancelled: false
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Record and transcribe new audio
|
|
95
|
+
*/ async recordAndTranscribeAudio(options) {
|
|
96
|
+
const outputDirectory = options.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
|
|
97
|
+
const storage = create({
|
|
98
|
+
log: this.logger.info
|
|
99
|
+
});
|
|
100
|
+
await storage.ensureDirectory(outputDirectory);
|
|
101
|
+
// Create a deterministic temp directory inside the configured output directory
|
|
102
|
+
const tempDir = path.join(outputDirectory, 'tmp');
|
|
103
|
+
await fs.mkdir(tempDir, {
|
|
104
|
+
recursive: true
|
|
105
|
+
});
|
|
106
|
+
// Use a timestamped filename to avoid collisions if multiple recordings run quickly
|
|
107
|
+
const audioFilePath = path.join(tempDir, `recording-${Date.now()}.wav`);
|
|
108
|
+
// Recording state variables
|
|
109
|
+
let recordingProcess = null;
|
|
110
|
+
let recordingFinished = false;
|
|
111
|
+
let recordingCancelled = false;
|
|
112
|
+
let recordingFailed = false;
|
|
113
|
+
let countdownInterval = null;
|
|
114
|
+
let remainingSeconds = 30;
|
|
115
|
+
let intendedRecordingTime = 30;
|
|
116
|
+
const maxRecordingTime = options.maxRecordingTime || 300; // 5 minutes default
|
|
117
|
+
const extensionTime = 30; // 30 seconds per extension
|
|
118
|
+
// Cleanup functions that need to be accessible in finally block
|
|
119
|
+
let keyHandler = null;
|
|
120
|
+
const originalRawMode = false;
|
|
121
|
+
const sigintHandler = null;
|
|
122
|
+
const cleanupKeyboardHandling = ()=>{
|
|
123
|
+
try {
|
|
124
|
+
if (keyHandler) {
|
|
125
|
+
process.stdin.removeListener('data', keyHandler);
|
|
126
|
+
keyHandler = null;
|
|
127
|
+
}
|
|
128
|
+
if (process.stdin.setRawMode) {
|
|
129
|
+
process.stdin.setRawMode(originalRawMode);
|
|
130
|
+
}
|
|
131
|
+
process.stdin.pause();
|
|
132
|
+
} catch (e) {
|
|
133
|
+
// Ignore cleanup errors
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
try {
|
|
137
|
+
this.logger.info('🎤 Starting recording... Speak now!');
|
|
138
|
+
this.logger.info('📋 Controls: ENTER=done, E=extend+30s, C/Ctrl+C=cancel');
|
|
139
|
+
// List available audio devices in debug mode
|
|
140
|
+
if (options.debug) {
|
|
141
|
+
await listAudioDevices();
|
|
142
|
+
}
|
|
143
|
+
// Recording control functions
|
|
144
|
+
const updateCountdownDisplay = ()=>{
|
|
145
|
+
const maxMinutes = Math.floor(maxRecordingTime / 60);
|
|
146
|
+
const intendedMinutes = Math.floor(intendedRecordingTime / 60);
|
|
147
|
+
const intendedSeconds = intendedRecordingTime % 60;
|
|
148
|
+
process.stdout.write(`\r⏱️ Recording: ${remainingSeconds}s left (${intendedMinutes}:${intendedSeconds.toString().padStart(2, '0')}/${maxMinutes}:00 max) [ENTER=done, E=+30s, C=cancel]`);
|
|
149
|
+
};
|
|
150
|
+
const extendRecording = ()=>{
|
|
151
|
+
const newTotal = intendedRecordingTime + extensionTime;
|
|
152
|
+
if (newTotal <= maxRecordingTime) {
|
|
153
|
+
intendedRecordingTime = newTotal;
|
|
154
|
+
remainingSeconds += extensionTime;
|
|
155
|
+
this.logger.info(`🔄 Extended recording by ${extensionTime}s (total: ${Math.floor(intendedRecordingTime / 60)}:${(intendedRecordingTime % 60).toString().padStart(2, '0')})`);
|
|
156
|
+
updateCountdownDisplay();
|
|
157
|
+
} else {
|
|
158
|
+
const canExtend = maxRecordingTime - intendedRecordingTime;
|
|
159
|
+
if (canExtend > 0) {
|
|
160
|
+
intendedRecordingTime = maxRecordingTime;
|
|
161
|
+
remainingSeconds += canExtend;
|
|
162
|
+
this.logger.info(`🔄 Extended recording by ${canExtend}s (maximum reached: ${Math.floor(maxRecordingTime / 60)}:${(maxRecordingTime % 60).toString().padStart(2, '0')})`);
|
|
163
|
+
updateCountdownDisplay();
|
|
164
|
+
} else {
|
|
165
|
+
this.logger.warn(`⚠️ Cannot extend: maximum recording time (${Math.floor(maxRecordingTime / 60)}:${(maxRecordingTime % 60).toString().padStart(2, '0')}) reached`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const stopRecording = async ()=>{
|
|
170
|
+
if (!recordingFinished && !recordingCancelled) {
|
|
171
|
+
recordingFinished = true;
|
|
172
|
+
if (countdownInterval) {
|
|
173
|
+
clearInterval(countdownInterval);
|
|
174
|
+
countdownInterval = null;
|
|
175
|
+
}
|
|
176
|
+
process.stdout.write('\r⏱️ Recording finished! \n');
|
|
177
|
+
if (recordingProcess && recordingProcess.kill) {
|
|
178
|
+
recordingProcess.kill('SIGTERM');
|
|
179
|
+
}
|
|
180
|
+
this.logger.info('🛑 Recording stopped');
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const cancelRecording = async ()=>{
|
|
184
|
+
if (!recordingFinished && !recordingCancelled) {
|
|
185
|
+
recordingCancelled = true;
|
|
186
|
+
if (countdownInterval) {
|
|
187
|
+
clearInterval(countdownInterval);
|
|
188
|
+
countdownInterval = null;
|
|
189
|
+
}
|
|
190
|
+
process.stdout.write('\r❌ Recording cancelled! \n');
|
|
191
|
+
if (recordingProcess && recordingProcess.kill) {
|
|
192
|
+
recordingProcess.kill('SIGTERM');
|
|
193
|
+
}
|
|
194
|
+
this.logger.info('❌ Audio recording cancelled by user');
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
// Set up keyboard input handling
|
|
198
|
+
let keyHandler = null;
|
|
199
|
+
let originalRawMode = false;
|
|
200
|
+
const setupKeyboardHandling = ()=>{
|
|
201
|
+
// Ensure stdin is properly configured
|
|
202
|
+
if (!process.stdin.readable) {
|
|
203
|
+
this.logger.warn('stdin is not readable, keyboard controls may not work');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Save original stdin state
|
|
207
|
+
originalRawMode = process.stdin.isRaw || false;
|
|
208
|
+
try {
|
|
209
|
+
process.stdin.setRawMode(true);
|
|
210
|
+
process.stdin.resume();
|
|
211
|
+
process.stdin.setEncoding('utf8');
|
|
212
|
+
keyHandler = (data)=>{
|
|
213
|
+
const key = data.toString();
|
|
214
|
+
const keyCode = key.charCodeAt(0);
|
|
215
|
+
if (options.debug) {
|
|
216
|
+
this.logger.debug('Key pressed: code=%d, char=%s', keyCode, JSON.stringify(key));
|
|
217
|
+
}
|
|
218
|
+
if (keyCode === 13 || keyCode === 10) {
|
|
219
|
+
process.stdout.write('\r✅ ENTER pressed - stopping recording... \n');
|
|
220
|
+
stopRecording();
|
|
221
|
+
} else if (key.toLowerCase() === 'e') {
|
|
222
|
+
extendRecording();
|
|
223
|
+
} else if (key.toLowerCase() === 'c' || keyCode === 3) {
|
|
224
|
+
process.stdout.write('\r❌ Cancelling recording... \n');
|
|
225
|
+
cancelRecording();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
process.stdin.on('data', keyHandler);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
this.logger.warn('Failed to setup keyboard handling: %s', error.message);
|
|
231
|
+
this.logger.info('You may need to use Ctrl+C to stop recording');
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
// Start countdown display
|
|
235
|
+
const startCountdown = ()=>{
|
|
236
|
+
updateCountdownDisplay();
|
|
237
|
+
countdownInterval = setInterval(()=>{
|
|
238
|
+
remainingSeconds--;
|
|
239
|
+
if (remainingSeconds > 0) {
|
|
240
|
+
updateCountdownDisplay();
|
|
241
|
+
} else {
|
|
242
|
+
process.stdout.write('\r⏱️ Recording: Time\'s up! \n');
|
|
243
|
+
if (countdownInterval) {
|
|
244
|
+
clearInterval(countdownInterval);
|
|
245
|
+
countdownInterval = null;
|
|
246
|
+
}
|
|
247
|
+
stopRecording();
|
|
248
|
+
}
|
|
249
|
+
}, 1000);
|
|
250
|
+
};
|
|
251
|
+
// Set up recording command
|
|
252
|
+
recordingProcess = await this.setupRecording(audioFilePath, maxRecordingTime, options);
|
|
253
|
+
if (options.debug) {
|
|
254
|
+
this.logger.debug('setupRecording returned: %s', recordingProcess ? 'process object' : 'null');
|
|
255
|
+
}
|
|
256
|
+
// Handle SIGINT for cleanup
|
|
257
|
+
process.on('SIGINT', cancelRecording);
|
|
258
|
+
// Start keyboard handling and countdown if we have a recording process
|
|
259
|
+
if (recordingProcess) {
|
|
260
|
+
setupKeyboardHandling();
|
|
261
|
+
startCountdown();
|
|
262
|
+
// Create a promise that resolves when user manually stops recording
|
|
263
|
+
const manualStopPromise = new Promise((resolve)=>{
|
|
264
|
+
const checkInterval = setInterval(()=>{
|
|
265
|
+
if (recordingFinished || recordingCancelled) {
|
|
266
|
+
clearInterval(checkInterval);
|
|
267
|
+
resolve();
|
|
268
|
+
}
|
|
269
|
+
}, 100);
|
|
270
|
+
});
|
|
271
|
+
// Create a promise that waits for the recording process to finish
|
|
272
|
+
const recordingProcessPromise = new Promise((resolve, reject)=>{
|
|
273
|
+
if (!recordingProcess) {
|
|
274
|
+
resolve();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const processStartTime = Date.now();
|
|
278
|
+
recordingProcess.on('exit', (code, signal)=>{
|
|
279
|
+
const processRunTime = Date.now() - processStartTime;
|
|
280
|
+
if (options.debug) {
|
|
281
|
+
this.logger.debug('Recording process exited with code %s, signal %s after %dms', code, signal, processRunTime);
|
|
282
|
+
}
|
|
283
|
+
// If the process exits very quickly with an error code, it likely failed to start recording
|
|
284
|
+
if (code !== 0 && processRunTime < 2000) {
|
|
285
|
+
this.logger.debug('Recording process failed early - runtime: %dms, exit code: %s', processRunTime, code);
|
|
286
|
+
recordingFailed = true;
|
|
287
|
+
}
|
|
288
|
+
resolve();
|
|
289
|
+
});
|
|
290
|
+
recordingProcess.on('error', (error)=>{
|
|
291
|
+
this.logger.error('Recording process error: %s', error.message);
|
|
292
|
+
recordingFailed = true;
|
|
293
|
+
reject(error);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
// Wait for either the recording to finish naturally or manual stop
|
|
297
|
+
try {
|
|
298
|
+
await Promise.race([
|
|
299
|
+
recordingProcessPromise,
|
|
300
|
+
manualStopPromise
|
|
301
|
+
]);
|
|
302
|
+
if (recordingFinished && recordingProcess && !recordingProcess.killed) {
|
|
303
|
+
recordingProcess.kill('SIGTERM');
|
|
304
|
+
await new Promise((resolve)=>setTimeout(resolve, 200));
|
|
305
|
+
if (!recordingProcess.killed) {
|
|
306
|
+
recordingProcess.kill('SIGKILL');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!recordingCancelled && !recordingFinished) {
|
|
310
|
+
if (countdownInterval) {
|
|
311
|
+
clearInterval(countdownInterval);
|
|
312
|
+
countdownInterval = null;
|
|
313
|
+
}
|
|
314
|
+
process.stdout.write('\r⏱️ Recording completed! \n');
|
|
315
|
+
this.logger.info('✅ Recording completed automatically');
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (!recordingCancelled && !recordingFinished) {
|
|
319
|
+
if (countdownInterval) {
|
|
320
|
+
clearInterval(countdownInterval);
|
|
321
|
+
countdownInterval = null;
|
|
322
|
+
}
|
|
323
|
+
if (error.signal === 'SIGTERM' || error.signal === 'SIGKILL') {
|
|
324
|
+
this.logger.debug('Recording process terminated as expected');
|
|
325
|
+
} else {
|
|
326
|
+
this.logger.warn('Recording process ended unexpectedly: %s', error.message);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Clean up keyboard input and process listeners
|
|
331
|
+
cleanupKeyboardHandling();
|
|
332
|
+
if (sigintHandler) ;
|
|
333
|
+
}
|
|
334
|
+
// If recording was cancelled, return early
|
|
335
|
+
if (recordingCancelled) {
|
|
336
|
+
return {
|
|
337
|
+
transcript: '',
|
|
338
|
+
cancelled: true
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// If recording failed (process exited with error too quickly), fail the command
|
|
342
|
+
if (recordingFailed) {
|
|
343
|
+
this.logger.error('❌ Audio recording failed to start or exited with an error');
|
|
344
|
+
this.logger.info('This usually means the audio device is busy, not accessible, or ffmpeg configuration is incorrect');
|
|
345
|
+
this.logger.info('💡 Try running "kodrdriv select-audio" to choose a different audio device');
|
|
346
|
+
throw new Error('Audio recording failed - cannot proceed with audio-commit command');
|
|
347
|
+
}
|
|
348
|
+
// Wait for the recording file to be fully written
|
|
349
|
+
if (recordingFinished) {
|
|
350
|
+
await new Promise((resolve)=>setTimeout(resolve, 500));
|
|
351
|
+
}
|
|
352
|
+
// Check if recording process failed early (before we try to verify the file)
|
|
353
|
+
try {
|
|
354
|
+
// Verify audio file was created
|
|
355
|
+
await this.verifyAudioFile(audioFilePath);
|
|
356
|
+
} catch (verifyError) {
|
|
357
|
+
// If the file doesn't exist, the recording process likely failed
|
|
358
|
+
this.logger.error('❌ Recording process failed - no audio file was created: %s', verifyError.message);
|
|
359
|
+
this.logger.info('This can happen if the audio device is busy, not accessible, or if ffmpeg is not properly configured');
|
|
360
|
+
this.logger.info('💡 Try running "kodrdriv select-audio" to choose a different audio device');
|
|
361
|
+
throw new Error(`Audio recording failed - ${verifyError.message}`);
|
|
362
|
+
}
|
|
363
|
+
// Transcribe the audio
|
|
364
|
+
const audioContext = await this.transcribeRecordedAudio(audioFilePath);
|
|
365
|
+
// Save files to output directory
|
|
366
|
+
const { audioOutputPath, transcriptOutputPath } = await this.saveRecordedFiles(audioFilePath, audioContext, outputDirectory);
|
|
367
|
+
if (!audioContext.trim()) {
|
|
368
|
+
this.logger.warn('No audio content was transcribed.');
|
|
369
|
+
return {
|
|
370
|
+
transcript: '',
|
|
371
|
+
audioFilePath: audioOutputPath,
|
|
372
|
+
transcriptFilePath: transcriptOutputPath,
|
|
373
|
+
cancelled: false
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
this.logger.info('📝 Audio recorded and transcribed successfully');
|
|
377
|
+
return {
|
|
378
|
+
transcript: audioContext,
|
|
379
|
+
audioFilePath: audioOutputPath,
|
|
380
|
+
transcriptFilePath: transcriptOutputPath,
|
|
381
|
+
cancelled: false
|
|
382
|
+
};
|
|
383
|
+
} catch (error) {
|
|
384
|
+
this.logger.error('Audio recording/transcription failed: %s', error.message);
|
|
385
|
+
// Re-throw the error so the command fails properly
|
|
386
|
+
throw error;
|
|
387
|
+
} finally{
|
|
388
|
+
// Cleanup is handled comprehensively in the cleanup function
|
|
389
|
+
await this.cleanup(countdownInterval, recordingProcess, tempDir, recordingFailed || options.keepTemp);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Set up recording command based on platform
|
|
394
|
+
*/ async setupRecording(audioFilePath, maxRecordingTime, options) {
|
|
395
|
+
let recordCommand;
|
|
396
|
+
let argsForSpawn;
|
|
397
|
+
if (process.platform === 'darwin') {
|
|
398
|
+
// macOS - try ffmpeg first
|
|
399
|
+
try {
|
|
400
|
+
await run('which ffmpeg');
|
|
401
|
+
const homeDeviceConfig = options.preferencesDirectory ? await loadAudioDeviceFromHomeConfig(options.preferencesDirectory) : null;
|
|
402
|
+
const audioDevice = options.audioDevice || (homeDeviceConfig === null || homeDeviceConfig === void 0 ? void 0 : homeDeviceConfig.audioDevice) || await detectBestAudioDevice();
|
|
403
|
+
// Get the correct input format for this device
|
|
404
|
+
const { getDeviceInputFormat } = await import('./devices.js');
|
|
405
|
+
const inputFormat = await getDeviceInputFormat(audioDevice);
|
|
406
|
+
if (!inputFormat) {
|
|
407
|
+
throw new Error(`Unable to find working format for audio device ${audioDevice}`);
|
|
408
|
+
}
|
|
409
|
+
// Build argument list explicitly. If the detected inputFormat is wrapped in
|
|
410
|
+
// quotes (e.g. ":0"), remove those quotes for the spawn call. The shell would
|
|
411
|
+
// normally strip them, but child_process.spawn passes the string verbatim to
|
|
412
|
+
// the executable, which causes ffmpeg to treat the quotes as part of the
|
|
413
|
+
// device name leading to failures such as "Video device not found".
|
|
414
|
+
const strippedInputFormat = inputFormat.startsWith('"') && inputFormat.endsWith('"') ? inputFormat.slice(1, -1) : inputFormat;
|
|
415
|
+
// Determine audio parameters based on stored capabilities (if available)
|
|
416
|
+
const channels = options.audioDevice ? undefined : homeDeviceConfig === null || homeDeviceConfig === void 0 ? void 0 : homeDeviceConfig.channels; // user may override manually via options
|
|
417
|
+
const sampleRate = options.audioDevice ? undefined : homeDeviceConfig === null || homeDeviceConfig === void 0 ? void 0 : homeDeviceConfig.sampleRate;
|
|
418
|
+
const ffmpegAudioArgs = [
|
|
419
|
+
'-c:a',
|
|
420
|
+
'pcm_s16le',
|
|
421
|
+
'-vn'
|
|
422
|
+
];
|
|
423
|
+
if (channels) {
|
|
424
|
+
ffmpegAudioArgs.push('-ac', String(channels));
|
|
425
|
+
} else {
|
|
426
|
+
// Default to mono when channel count is unknown to reduce size
|
|
427
|
+
ffmpegAudioArgs.push('-ac', '1');
|
|
428
|
+
}
|
|
429
|
+
if (sampleRate) {
|
|
430
|
+
ffmpegAudioArgs.push('-ar', String(sampleRate));
|
|
431
|
+
}
|
|
432
|
+
const ffmpegArgs = [
|
|
433
|
+
'-f',
|
|
434
|
+
'avfoundation',
|
|
435
|
+
'-i',
|
|
436
|
+
strippedInputFormat,
|
|
437
|
+
...ffmpegAudioArgs,
|
|
438
|
+
'-t',
|
|
439
|
+
String(maxRecordingTime),
|
|
440
|
+
'-y',
|
|
441
|
+
audioFilePath
|
|
442
|
+
];
|
|
443
|
+
// Store for later spawn
|
|
444
|
+
recordCommand = `ffmpeg ${ffmpegArgs.join(' ')}`;
|
|
445
|
+
argsForSpawn = ffmpegArgs;
|
|
446
|
+
// Log the exact command being executed
|
|
447
|
+
this.logger.info(`🔧 Executing recording command: ${recordCommand}`);
|
|
448
|
+
if (options.audioDevice) {
|
|
449
|
+
this.logger.info(`🎙️ Using audio device ${audioDevice} (from configuration) with format ${inputFormat}`);
|
|
450
|
+
} else if (homeDeviceConfig) {
|
|
451
|
+
this.logger.info(`🎙️ Using audio device ${audioDevice} (${homeDeviceConfig.audioDeviceName}) with format ${inputFormat}`);
|
|
452
|
+
} else {
|
|
453
|
+
this.logger.info(`🎙️ Using audio device ${audioDevice} (auto-detected) with format ${inputFormat}`);
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
// Try sox/rec as fallback
|
|
457
|
+
try {
|
|
458
|
+
await run('which rec');
|
|
459
|
+
recordCommand = `rec -c 1 -t wav "${audioFilePath}" trim 0 ${maxRecordingTime}`;
|
|
460
|
+
this.logger.info(`🔧 Executing recording command (sox fallback): ${recordCommand}`);
|
|
461
|
+
} catch {
|
|
462
|
+
throw new Error('MANUAL_RECORDING_NEEDED');
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} else if (process.platform === 'win32') {
|
|
466
|
+
// Windows - use ffmpeg
|
|
467
|
+
try {
|
|
468
|
+
await run('where ffmpeg');
|
|
469
|
+
recordCommand = `ffmpeg -f dshow -i audio="Microphone" -ac 1 -c:a pcm_s16le -vn -t ${maxRecordingTime} -y "${audioFilePath}"`;
|
|
470
|
+
this.logger.info(`🔧 Executing recording command: ${recordCommand}`);
|
|
471
|
+
} catch {
|
|
472
|
+
throw new Error('MANUAL_RECORDING_NEEDED');
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
// Linux - use arecord or ffmpeg
|
|
476
|
+
try {
|
|
477
|
+
await run('which arecord');
|
|
478
|
+
recordCommand = `arecord -f cd -t wav -d ${maxRecordingTime} "${audioFilePath}"`;
|
|
479
|
+
this.logger.info(`🔧 Executing recording command: ${recordCommand}`);
|
|
480
|
+
} catch {
|
|
481
|
+
try {
|
|
482
|
+
await run('which ffmpeg');
|
|
483
|
+
recordCommand = `ffmpeg -f alsa -i default -ac 1 -c:a pcm_s16le -vn -t ${maxRecordingTime} -y "${audioFilePath}"`;
|
|
484
|
+
this.logger.info(`🔧 Executing recording command: ${recordCommand}`);
|
|
485
|
+
} catch {
|
|
486
|
+
throw new Error('MANUAL_RECORDING_NEEDED');
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
var _recordingProcess_stderr;
|
|
492
|
+
// Ensure the output directory exists (equivalent to `mkdir -p`)
|
|
493
|
+
await fs.mkdir(path.dirname(audioFilePath), {
|
|
494
|
+
recursive: true
|
|
495
|
+
});
|
|
496
|
+
// Use spawn instead of exec for better process control
|
|
497
|
+
const { spawn } = await import('child_process');
|
|
498
|
+
// If argsForSpawn was prepared (preferred path), use it; otherwise fall back to splitting
|
|
499
|
+
const command = 'ffmpeg';
|
|
500
|
+
const args = argsForSpawn !== null && argsForSpawn !== void 0 ? argsForSpawn : recordCommand.split(' ').slice(1);
|
|
501
|
+
this.logger.debug(`🔧 Spawning process: ${command} with args: ${JSON.stringify(args)}`);
|
|
502
|
+
const recordingProcess = spawn(command, args, {
|
|
503
|
+
stdio: [
|
|
504
|
+
'ignore',
|
|
505
|
+
'ignore',
|
|
506
|
+
'pipe'
|
|
507
|
+
],
|
|
508
|
+
detached: false
|
|
509
|
+
});
|
|
510
|
+
// Enhanced error handling and stderr capture
|
|
511
|
+
let stderrBuffer = '';
|
|
512
|
+
recordingProcess.on('error', (error)=>{
|
|
513
|
+
this.logger.error('Recording process error: %s', error.message);
|
|
514
|
+
this.logger.error('Full error details: %s', JSON.stringify(error));
|
|
515
|
+
});
|
|
516
|
+
(_recordingProcess_stderr = recordingProcess.stderr) === null || _recordingProcess_stderr === void 0 ? void 0 : _recordingProcess_stderr.on('data', (data)=>{
|
|
517
|
+
const output = data.toString().trim();
|
|
518
|
+
stderrBuffer += output + '\n';
|
|
519
|
+
if (options.debug) {
|
|
520
|
+
this.logger.debug('Recording process stderr: %s', output);
|
|
521
|
+
} else {
|
|
522
|
+
// In non-debug mode, still capture critical errors
|
|
523
|
+
if (output.toLowerCase().includes('error') || output.toLowerCase().includes('failed') || output.toLowerCase().includes('permission') || output.toLowerCase().includes('busy') || output.toLowerCase().includes('device')) {
|
|
524
|
+
this.logger.warn('Recording process stderr: %s', output);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
// Enhanced exit handling with stderr output
|
|
529
|
+
recordingProcess.on('exit', (code, signal)=>{
|
|
530
|
+
if (code !== 0) {
|
|
531
|
+
this.logger.error('🚨 Recording process exited with error code %s, signal %s', code, signal);
|
|
532
|
+
if (stderrBuffer.trim()) {
|
|
533
|
+
this.logger.error('🚨 Recording process stderr output:\n%s', stderrBuffer);
|
|
534
|
+
}
|
|
535
|
+
} else if (options.debug) {
|
|
536
|
+
this.logger.debug('Recording process exited successfully with code %s', code);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
return recordingProcess;
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (error.message === 'MANUAL_RECORDING_NEEDED') {
|
|
542
|
+
this.showManualRecordingInstructions(audioFilePath);
|
|
543
|
+
await this.waitForManualRecording();
|
|
544
|
+
return null;
|
|
545
|
+
} else {
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Show instructions for manual recording
|
|
552
|
+
*/ showManualRecordingInstructions(audioFilePath) {
|
|
553
|
+
this.logger.warn('⚠️ Automatic recording not available on this system.');
|
|
554
|
+
this.logger.warn('📱 Please record audio manually using your system\'s built-in tools:');
|
|
555
|
+
this.logger.warn('');
|
|
556
|
+
if (process.platform === 'darwin') {
|
|
557
|
+
this.logger.warn('🍎 macOS options:');
|
|
558
|
+
this.logger.warn(' 1. Use QuickTime Player: File → New Audio Recording');
|
|
559
|
+
this.logger.warn(' 2. Use Voice Memos app');
|
|
560
|
+
this.logger.warn(' 3. Install ffmpeg: brew install ffmpeg');
|
|
561
|
+
this.logger.warn(' 4. Install sox: brew install sox');
|
|
562
|
+
} else if (process.platform === 'win32') {
|
|
563
|
+
this.logger.warn('🪟 Windows options:');
|
|
564
|
+
this.logger.warn(' 1. Use Voice Recorder app');
|
|
565
|
+
this.logger.warn(' 2. Install ffmpeg: https://ffmpeg.org/download.html');
|
|
566
|
+
} else {
|
|
567
|
+
this.logger.warn('🐧 Linux options:');
|
|
568
|
+
this.logger.warn(' 1. Install alsa-utils: sudo apt install alsa-utils');
|
|
569
|
+
this.logger.warn(' 2. Install ffmpeg: sudo apt install ffmpeg');
|
|
570
|
+
}
|
|
571
|
+
this.logger.warn('');
|
|
572
|
+
this.logger.warn(`💾 Save your recording as: ${audioFilePath}`);
|
|
573
|
+
this.logger.warn('🎵 Recommended format: WAV, 44.1kHz, mono or stereo');
|
|
574
|
+
this.logger.warn('');
|
|
575
|
+
this.logger.warn('⌨️ Press ENTER when you have saved the audio file...');
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Wait for user to complete manual recording
|
|
579
|
+
*/ async waitForManualRecording() {
|
|
580
|
+
return new Promise((resolve)=>{
|
|
581
|
+
process.stdin.setRawMode(true);
|
|
582
|
+
process.stdin.resume();
|
|
583
|
+
const enterHandler = (key)=>{
|
|
584
|
+
if (key[0] === 13) {
|
|
585
|
+
process.stdin.setRawMode(false);
|
|
586
|
+
process.stdin.pause();
|
|
587
|
+
process.stdin.removeListener('data', enterHandler);
|
|
588
|
+
resolve();
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
process.stdin.on('data', enterHandler);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Verify that the audio file was created successfully
|
|
596
|
+
*/ async verifyAudioFile(audioFilePath) {
|
|
597
|
+
try {
|
|
598
|
+
await fs.access(audioFilePath);
|
|
599
|
+
const stats = await fs.stat(audioFilePath);
|
|
600
|
+
if (stats.size === 0) {
|
|
601
|
+
throw new Error('Audio file is empty');
|
|
602
|
+
}
|
|
603
|
+
this.logger.info('✅ Audio file created successfully (%d bytes)', stats.size);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
throw new Error(`Failed to create audio file: ${error.message}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Transcribe recorded audio
|
|
610
|
+
*/ async transcribeRecordedAudio(audioFilePath) {
|
|
611
|
+
this.logger.info('🎯 Transcribing audio...');
|
|
612
|
+
this.logger.info('⏳ This may take a few seconds depending on audio length...');
|
|
613
|
+
const transcription = await transcribeAudio(audioFilePath);
|
|
614
|
+
const audioContext = transcription.text;
|
|
615
|
+
this.logger.info('✅ Audio transcribed successfully');
|
|
616
|
+
this.logger.debug('Transcription: %s', audioContext);
|
|
617
|
+
return audioContext;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Save transcript file
|
|
621
|
+
*/ async saveTranscript(audioContext, sourceFilePath, outputDirectory) {
|
|
622
|
+
if (!audioContext.trim()) return undefined;
|
|
623
|
+
try {
|
|
624
|
+
this.logger.info('💾 Saving transcript...');
|
|
625
|
+
const storage = create({
|
|
626
|
+
log: this.logger.info
|
|
627
|
+
});
|
|
628
|
+
await storage.ensureDirectory(outputDirectory);
|
|
629
|
+
const transcriptOutputFilename = getTimestampedTranscriptFilename();
|
|
630
|
+
const transcriptOutputPath = getOutputPath(outputDirectory, transcriptOutputFilename);
|
|
631
|
+
const transcriptContent = `# Audio Transcript\n\n**Source:** ${sourceFilePath}\n**Processed:** ${new Date().toISOString()}\n\n**Transcript:**\n\n${audioContext}`;
|
|
632
|
+
await storage.writeFile(transcriptOutputPath, transcriptContent, 'utf-8');
|
|
633
|
+
this.logger.debug('Saved transcript: %s', transcriptOutputPath);
|
|
634
|
+
return transcriptOutputPath;
|
|
635
|
+
} catch (error) {
|
|
636
|
+
this.logger.warn('Failed to save transcript file: %s', error.message);
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Save recorded audio file and transcript
|
|
642
|
+
*/ async saveRecordedFiles(audioFilePath, audioContext, outputDirectory) {
|
|
643
|
+
try {
|
|
644
|
+
this.logger.info('💾 Saving audio file and transcript...');
|
|
645
|
+
const storage = create({
|
|
646
|
+
log: this.logger.info
|
|
647
|
+
});
|
|
648
|
+
await storage.ensureDirectory(outputDirectory);
|
|
649
|
+
// Save audio file copy
|
|
650
|
+
const audioOutputFilename = getTimestampedAudioFilename();
|
|
651
|
+
const audioOutputPath = getOutputPath(outputDirectory, audioOutputFilename);
|
|
652
|
+
await fs.copyFile(audioFilePath, audioOutputPath);
|
|
653
|
+
this.logger.debug('Saved audio file: %s', audioOutputPath);
|
|
654
|
+
// Save transcript
|
|
655
|
+
let transcriptOutputPath;
|
|
656
|
+
if (audioContext.trim()) {
|
|
657
|
+
const transcriptOutputFilename = getTimestampedTranscriptFilename();
|
|
658
|
+
transcriptOutputPath = getOutputPath(outputDirectory, transcriptOutputFilename);
|
|
659
|
+
const transcriptContent = `# Audio Transcript\n\n**Recorded:** ${new Date().toISOString()}\n\n**Transcript:**\n\n${audioContext}`;
|
|
660
|
+
await storage.writeFile(transcriptOutputPath, transcriptContent, 'utf-8');
|
|
661
|
+
this.logger.debug('Saved transcript: %s', transcriptOutputPath);
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
audioOutputPath,
|
|
665
|
+
transcriptOutputPath
|
|
666
|
+
};
|
|
667
|
+
} catch (error) {
|
|
668
|
+
this.logger.warn('Failed to save audio/transcript files: %s', error.message);
|
|
669
|
+
return {};
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Clean up resources
|
|
674
|
+
*/ async cleanup(countdownInterval, recordingProcess, tempDir, keepTemp = false) {
|
|
675
|
+
try {
|
|
676
|
+
// Clear countdown interval
|
|
677
|
+
if (countdownInterval) {
|
|
678
|
+
clearInterval(countdownInterval);
|
|
679
|
+
}
|
|
680
|
+
// Reset stdin thoroughly - this is critical for preventing hanging
|
|
681
|
+
try {
|
|
682
|
+
if (process.stdin.setRawMode) {
|
|
683
|
+
process.stdin.setRawMode(false);
|
|
684
|
+
}
|
|
685
|
+
process.stdin.pause();
|
|
686
|
+
process.stdin.removeAllListeners('data');
|
|
687
|
+
process.stdin.removeAllListeners('keypress');
|
|
688
|
+
process.stdin.removeAllListeners('readable');
|
|
689
|
+
process.stdin.removeAllListeners('end');
|
|
690
|
+
process.stdin.removeAllListeners('close');
|
|
691
|
+
// Force stdin to unpipe if it was piped
|
|
692
|
+
if (process.stdin.unpipe) {
|
|
693
|
+
process.stdin.unpipe();
|
|
694
|
+
}
|
|
695
|
+
// Completely detach stdin from the event-loop
|
|
696
|
+
if (typeof process.stdin.unref === 'function') {
|
|
697
|
+
process.stdin.unref();
|
|
698
|
+
}
|
|
699
|
+
} catch (stdinError) {
|
|
700
|
+
// Ignore stdin cleanup errors
|
|
701
|
+
}
|
|
702
|
+
// Remove ALL process event listeners to prevent hanging
|
|
703
|
+
process.removeAllListeners('SIGINT');
|
|
704
|
+
process.removeAllListeners('SIGTERM');
|
|
705
|
+
process.removeAllListeners('SIGQUIT');
|
|
706
|
+
process.removeAllListeners('SIGHUP');
|
|
707
|
+
process.removeAllListeners('exit');
|
|
708
|
+
process.removeAllListeners('beforeExit');
|
|
709
|
+
// Kill recording process aggressively
|
|
710
|
+
if (recordingProcess && !recordingProcess.killed) {
|
|
711
|
+
try {
|
|
712
|
+
recordingProcess.kill('SIGTERM');
|
|
713
|
+
// Give it a very short time to terminate gracefully
|
|
714
|
+
await new Promise((resolve)=>setTimeout(resolve, 50));
|
|
715
|
+
if (!recordingProcess.killed) {
|
|
716
|
+
recordingProcess.kill('SIGKILL');
|
|
717
|
+
}
|
|
718
|
+
// Destroy and detach any stdio streams
|
|
719
|
+
if (recordingProcess.stderr) {
|
|
720
|
+
try {
|
|
721
|
+
recordingProcess.stderr.removeAllListeners();
|
|
722
|
+
recordingProcess.stderr.destroy();
|
|
723
|
+
if (typeof recordingProcess.stderr.unref === 'function') {
|
|
724
|
+
recordingProcess.stderr.unref();
|
|
725
|
+
}
|
|
726
|
+
} catch {
|
|
727
|
+
/* ignore */ }
|
|
728
|
+
}
|
|
729
|
+
// Remove all listeners from the recording process itself and detach from event-loop
|
|
730
|
+
recordingProcess.removeAllListeners();
|
|
731
|
+
if (typeof recordingProcess.unref === 'function') {
|
|
732
|
+
recordingProcess.unref();
|
|
733
|
+
}
|
|
734
|
+
} catch (killError) {
|
|
735
|
+
// Ignore kill errors
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// Clean up temporary directory unless instructed to keep it (e.g., on failure)
|
|
739
|
+
if (!keepTemp) {
|
|
740
|
+
try {
|
|
741
|
+
await fs.rm(tempDir, {
|
|
742
|
+
recursive: true,
|
|
743
|
+
force: true
|
|
744
|
+
});
|
|
745
|
+
} catch (fsError) {
|
|
746
|
+
// Ignore filesystem cleanup errors
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
this.logger.debug('Keeping temporary directory for inspection: %s', tempDir);
|
|
750
|
+
}
|
|
751
|
+
} catch (cleanupError) {
|
|
752
|
+
this.logger.debug('Cleanup warning: %s', cleanupError.message);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
constructor(){
|
|
756
|
+
_define_property(this, "logger", getLogger());
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Create a new audio processor instance
|
|
761
|
+
*/ const createAudioProcessor = ()=>{
|
|
762
|
+
return new AudioProcessor();
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
export { AudioProcessor, createAudioProcessor };
|
|
766
|
+
//# sourceMappingURL=processor.js.map
|