@grunnverk/commands-audio 1.0.0

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 ADDED
@@ -0,0 +1,66 @@
1
+ # @grunnverk/commands-audio
2
+
3
+ Audio transcription and voice commands for kodrdriv.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @grunnverk/commands-audio
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ### select-audio
14
+
15
+ Select and configure audio input device.
16
+
17
+ ```bash
18
+ kodrdriv select-audio
19
+ ```
20
+
21
+ ### audio-commit
22
+
23
+ Record audio and use transcription to create commit message.
24
+
25
+ ```bash
26
+ kodrdriv audio-commit
27
+ kodrdriv audio-commit --file recording.wav
28
+ ```
29
+
30
+ ### audio-review
31
+
32
+ Record audio or process audio files for code review context.
33
+
34
+ ```bash
35
+ kodrdriv audio-review
36
+ kodrdriv audio-review --file feedback.wav
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```typescript
42
+ import { selectAudio, audioCommit, audioReview } from '@grunnverk/commands-audio';
43
+
44
+ // Select audio device
45
+ await selectAudio(config);
46
+
47
+ // Create commit from audio
48
+ await audioCommit(config);
49
+
50
+ // Add audio context to review
51
+ await audioReview(config);
52
+ ```
53
+
54
+ ## Dependencies
55
+
56
+ - `@grunnverk/core` - Core utilities and types
57
+ - `@grunnverk/audio-tools` - Audio recording and processing
58
+ - `@grunnverk/ai-service` - Transcription service
59
+ - `@theunwalked/unplayable` - Audio capture library
60
+
61
+ ## License
62
+
63
+ Apache-2.0
64
+
65
+
66
+ <!-- Build: 2026-01-15 15:59:12 UTC -->
@@ -0,0 +1,2 @@
1
+ export * from './src/index'
2
+ export {}
package/dist/index.js ADDED
@@ -0,0 +1,447 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import { getDryRunLogger, getLogger, getTimestampedAudioFilename, createStorageAdapter, createLoggerAdapter } from '@grunnverk/core';
4
+ import { selectAndConfigureAudioDevice, processAudio } from '@theunwalked/unplayable';
5
+ import { UserCancellationError, CancellationError, createStorage } from '@grunnverk/shared';
6
+ import { transcribeAudio } from '@grunnverk/ai-service';
7
+ import { commit, review } from '@grunnverk/commands-git';
8
+ import { createAudioRecordingCountdown, archiveAudio } from '@grunnverk/audio-tools';
9
+
10
+ const getUnplayableConfigPath = ()=>{
11
+ try {
12
+ return path.join(os.homedir(), '.unplayable', 'audio-device.json');
13
+ } catch (error) {
14
+ throw new Error(`Failed to determine home directory: ${error.message}`);
15
+ }
16
+ };
17
+ const execute$2 = async (runConfig)=>{
18
+ const isDryRun = runConfig.dryRun || false;
19
+ const logger = getDryRunLogger(isDryRun);
20
+ if (isDryRun) {
21
+ try {
22
+ const configPath = getUnplayableConfigPath();
23
+ logger.info('AUDIO_SELECT_DRY_RUN: Would start audio device selection | Mode: dry-run | Purpose: Choose input device');
24
+ logger.info('AUDIO_SELECT_SAVE_DRY_RUN: Would save device to config | Mode: dry-run | Path: %s', configPath);
25
+ return 'Audio device selection completed (dry run)';
26
+ } catch (error) {
27
+ logger.warn('AUDIO_SELECT_CONFIG_PATH_ERROR: Error determining config path | Error: %s | Impact: Cannot show save location', error.message);
28
+ return 'Audio device selection completed (dry run)';
29
+ }
30
+ }
31
+ try {
32
+ const preferencesDir = path.join(os.homedir(), '.unplayable');
33
+ const result = await selectAndConfigureAudioDevice(preferencesDir, logger, runConfig.debug);
34
+ return result;
35
+ } catch (error) {
36
+ // Check if this is a home directory error
37
+ if (error.message && error.message.includes('Failed to determine home directory')) {
38
+ logger.error('AUDIO_SELECT_FAILED: Audio device selection failed | Error: %s', error.message);
39
+ throw new Error(`Failed to determine home directory: ${error.message}`);
40
+ } else {
41
+ const errorMessage = error.message || error.toString();
42
+ logger.error('AUDIO_SELECT_COMMAND_FAILED: Audio device selection command failed | Error: %s | Status: failed', errorMessage);
43
+ throw new Error(`Audio device selection failed: ${errorMessage}`);
44
+ }
45
+ }
46
+ };
47
+
48
+ const executeInternal = async (runConfig)=>{
49
+ var _runConfig_commit;
50
+ const isDryRun = runConfig.dryRun || false;
51
+ const logger = getDryRunLogger(isDryRun);
52
+ if (isDryRun) {
53
+ var _runConfig_audioCommit;
54
+ if ((_runConfig_audioCommit = runConfig.audioCommit) === null || _runConfig_audioCommit === void 0 ? void 0 : _runConfig_audioCommit.file) {
55
+ logger.info('AUDIO_COMMIT_FILE_DRY_RUN: Would process audio file | Mode: dry-run | File: %s | Action: Transcribe + generate commit', runConfig.audioCommit.file);
56
+ logger.info('AUDIO_COMMIT_WORKFLOW_DRY_RUN: Would transcribe and generate message | Mode: dry-run | Purpose: Commit message from audio');
57
+ } else {
58
+ logger.info('AUDIO_COMMIT_RECORD_DRY_RUN: Would start audio recording | Mode: dry-run | Purpose: Commit context');
59
+ logger.info('AUDIO_COMMIT_TRANSCRIPT_DRY_RUN: Would transcribe and generate | Mode: dry-run | Purpose: Extract commit message');
60
+ }
61
+ logger.info('AUDIO_COMMIT_DELEGATE_DRY_RUN: Would delegate to regular commit command | Mode: dry-run | Next: Standard commit flow');
62
+ // Return preview without calling real commands
63
+ return 'DRY RUN: Would process audio, transcribe it, and generate commit message with audio context';
64
+ }
65
+ let audioContext;
66
+ try {
67
+ var _runConfig_audioCommit1, _runConfig_audioCommit2, _runConfig_audioCommit3, _runConfig_audioCommit4;
68
+ // Step 1: Record audio using unplayable with new key handling
69
+ logger.info('AUDIO_COMMIT_RECORDING_STARTING: Starting audio recording | Purpose: Capture commit context | Tool: unplayable');
70
+ if (!((_runConfig_audioCommit1 = runConfig.audioCommit) === null || _runConfig_audioCommit1 === void 0 ? void 0 : _runConfig_audioCommit1.file)) {
71
+ logger.info('AUDIO_COMMIT_RECORDING_ACTIVE: Recording in progress | Action: Press ENTER to stop | Alternative: Press C to cancel');
72
+ }
73
+ // Start countdown timer if recording (not processing a file) and maxRecordingTime is set
74
+ const maxRecordingTime = (_runConfig_audioCommit2 = runConfig.audioCommit) === null || _runConfig_audioCommit2 === void 0 ? void 0 : _runConfig_audioCommit2.maxRecordingTime;
75
+ const isRecording = !((_runConfig_audioCommit3 = runConfig.audioCommit) === null || _runConfig_audioCommit3 === void 0 ? void 0 : _runConfig_audioCommit3.file);
76
+ let countdownTimer = null;
77
+ if (isRecording && maxRecordingTime && maxRecordingTime > 0) {
78
+ countdownTimer = createAudioRecordingCountdown(maxRecordingTime);
79
+ // Start countdown timer in parallel with recording
80
+ countdownTimer.start().catch(()=>{
81
+ // Timer completed naturally, no action needed
82
+ });
83
+ }
84
+ let audioResult;
85
+ try {
86
+ var _runConfig_audioCommit5, _runConfig_audioCommit6;
87
+ // Use processAudio with proper configuration
88
+ audioResult = await processAudio({
89
+ file: (_runConfig_audioCommit5 = runConfig.audioCommit) === null || _runConfig_audioCommit5 === void 0 ? void 0 : _runConfig_audioCommit5.file,
90
+ maxRecordingTime: (_runConfig_audioCommit6 = runConfig.audioCommit) === null || _runConfig_audioCommit6 === void 0 ? void 0 : _runConfig_audioCommit6.maxRecordingTime,
91
+ outputDirectory: runConfig.outputDirectory || 'output',
92
+ debug: runConfig.debug
93
+ });
94
+ } finally{
95
+ // Stop countdown timer if it was running
96
+ if (countdownTimer) {
97
+ countdownTimer.stop();
98
+ }
99
+ }
100
+ // Check if recording was cancelled
101
+ if (audioResult.cancelled) {
102
+ logger.info('AUDIO_COMMIT_CANCELLED: Audio commit cancelled by user | Reason: User choice | Status: aborted');
103
+ throw new UserCancellationError('Audio commit cancelled by user');
104
+ }
105
+ // Step 2: Get the audio file path from the result
106
+ let audioFilePath;
107
+ if ((_runConfig_audioCommit4 = runConfig.audioCommit) === null || _runConfig_audioCommit4 === void 0 ? void 0 : _runConfig_audioCommit4.file) {
108
+ // Use the provided file path
109
+ audioFilePath = runConfig.audioCommit.file;
110
+ } else if (audioResult.audioFilePath) {
111
+ // Use the file path returned by processAudio
112
+ audioFilePath = audioResult.audioFilePath;
113
+ } else {
114
+ // Fallback to generated filename (this should rarely happen now)
115
+ const outputDir = runConfig.outputDirectory || 'output';
116
+ audioFilePath = path.join(outputDir, getTimestampedAudioFilename());
117
+ logger.warn('AUDIO_COMMIT_FILENAME_GENERATED: Using generated filename for audio | Filename: %s | Warning: May not match actual file from unplayable', audioFilePath);
118
+ logger.warn('AUDIO_COMMIT_FILENAME_NOTE: Filename mismatch possible | Tool: unplayable | Impact: May need manual file lookup');
119
+ }
120
+ // Step 3: Use ai-service transcription functionality
121
+ logger.info('AUDIO_COMMIT_TRANSCRIBING: Transcribing audio locally | Service: OpenAI Whisper | Mode: local | Purpose: Convert speech to text');
122
+ const outputDir = runConfig.outputDirectory || 'output';
123
+ const aiStorageAdapter = createStorageAdapter(outputDir);
124
+ const aiLogger = createLoggerAdapter(isDryRun);
125
+ const transcription = await transcribeAudio(audioFilePath, {
126
+ model: "whisper-1",
127
+ debug: runConfig.debug,
128
+ storage: aiStorageAdapter,
129
+ logger: aiLogger,
130
+ onArchive: async (audioPath, transcriptionText)=>{
131
+ const outputDir = path.join(runConfig.outputDirectory || 'output', 'kodrdriv');
132
+ await archiveAudio(audioPath, transcriptionText, outputDir);
133
+ }
134
+ });
135
+ audioContext = transcription.text;
136
+ if (!audioContext.trim()) {
137
+ logger.warn('AUDIO_COMMIT_NO_CONTENT: No audio content transcribed | Reason: Empty or invalid | Action: Proceeding without audio context');
138
+ audioContext = '';
139
+ } else {
140
+ logger.info('AUDIO_COMMIT_TRANSCRIPT_SUCCESS: Successfully transcribed audio | Tool: kodrdriv | Length: ' + audioContext.length + ' characters | Status: ready');
141
+ logger.debug('Transcribed text: %s', audioContext);
142
+ }
143
+ } catch (error) {
144
+ // Re-throw cancellation errors properly
145
+ if (error instanceof UserCancellationError) {
146
+ throw error;
147
+ }
148
+ // Convert old CancellationError to new UserCancellationError
149
+ if (error.name === 'CancellationError' || error instanceof CancellationError) {
150
+ throw new UserCancellationError(error.message);
151
+ }
152
+ logger.error('AUDIO_COMMIT_PROCESSING_FAILED: Audio processing failed | Error: %s | Impact: No audio context available', error.message);
153
+ logger.info('AUDIO_COMMIT_FALLBACK: Proceeding without audio context | Mode: fallback | Next: Standard commit generation');
154
+ audioContext = '';
155
+ }
156
+ // Now delegate to the regular commit command with the audio context
157
+ logger.info('AUDIO_COMMIT_GENERATING: Generating commit message with audio context | Source: transcript | Purpose: AI-generated commit message');
158
+ const result = await commit({
159
+ ...runConfig,
160
+ commit: {
161
+ ...runConfig.commit,
162
+ direction: audioContext.trim() || ((_runConfig_commit = runConfig.commit) === null || _runConfig_commit === void 0 ? void 0 : _runConfig_commit.direction) || ''
163
+ }
164
+ });
165
+ return result;
166
+ };
167
+ const execute$1 = async (runConfig)=>{
168
+ try {
169
+ return await executeInternal(runConfig);
170
+ } catch (error) {
171
+ const logger = getLogger();
172
+ // Handle user cancellation gracefully - don't exit process
173
+ if (error instanceof UserCancellationError) {
174
+ logger.info('AUDIO_COMMIT_ERROR: Error during audio commit | Error: ' + error.message);
175
+ throw error; // Let calling code handle this
176
+ }
177
+ // Handle other errors - don't exit process
178
+ logger.error(`AUDIO_COMMIT_FAILED: Audio commit command failed | Error: ${error.message} | Impact: Commit not generated`);
179
+ if (error.cause) {
180
+ logger.debug(`Caused by: ${error.cause.message}`);
181
+ }
182
+ throw error; // Let calling code handle this
183
+ }
184
+ };
185
+
186
+ // Common audio file extensions
187
+ const AUDIO_EXTENSIONS = [
188
+ '.wav',
189
+ '.mp3',
190
+ '.m4a',
191
+ '.aac',
192
+ '.flac',
193
+ '.ogg',
194
+ '.wma'
195
+ ];
196
+ /**
197
+ * Discover audio files in a directory
198
+ */ const discoverAudioFiles = async (directory)=>{
199
+ const logger = getLogger();
200
+ const storage = createStorage();
201
+ try {
202
+ if (!await storage.isDirectoryReadable(directory)) {
203
+ throw new Error(`Directory not readable: ${directory}`);
204
+ }
205
+ const allFiles = await storage.listFiles(directory);
206
+ const audioFiles = allFiles.filter((file)=>AUDIO_EXTENSIONS.includes(path.extname(file).toLowerCase())).map((file)=>path.join(directory, file)).sort(); // Sort for consistent processing order
207
+ logger.info(`AUDIO_REVIEW_FILES_FOUND: Found audio files in directory | Count: ${audioFiles.length} | Directory: ${directory} | Status: ready-for-processing`);
208
+ logger.debug('Audio files found: %s', audioFiles.join(', '));
209
+ return audioFiles;
210
+ } catch (error) {
211
+ logger.error('AUDIO_REVIEW_DISCOVERY_FAILED: Failed to discover audio files | Directory: %s | Error: %s | Impact: Cannot process batch', directory, error.message);
212
+ throw error;
213
+ }
214
+ };
215
+ /**
216
+ * Process a single audio file for review
217
+ */ const processSingleAudioFile = async (audioFilePath, runConfig)=>{
218
+ const logger = getLogger();
219
+ try {
220
+ var _runConfig_audioReview, _runConfig_audioReview1, _runConfig_audioReview2, _runConfig_audioReview3, _runConfig_audioReview4, _runConfig_audioReview5, _runConfig_audioReview6, _runConfig_audioReview7, _runConfig_audioReview8, _runConfig_audioReview9;
221
+ logger.info('AUDIO_REVIEW_PROCESSING: Processing audio file | File: %s | Action: Transcribe and analyze', path.basename(audioFilePath));
222
+ // Use kodrdriv's transcription functionality
223
+ logger.info('AUDIO_REVIEW_TRANSCRIBING: Transcribing audio using OpenAI Whisper | Service: OpenAI Whisper | Purpose: Convert speech to text');
224
+ const outputDir = runConfig.outputDirectory || 'output';
225
+ const aiStorageAdapter = createStorageAdapter(outputDir);
226
+ const aiLogger = createLoggerAdapter(runConfig.dryRun || false);
227
+ const transcription = await transcribeAudio(audioFilePath, {
228
+ model: "whisper-1",
229
+ debug: runConfig.debug,
230
+ storage: aiStorageAdapter,
231
+ logger: aiLogger,
232
+ onArchive: async (audioPath, transcriptionText)=>{
233
+ const outputDir = path.join(runConfig.outputDirectory || 'output', 'kodrdriv');
234
+ await archiveAudio(audioPath, transcriptionText, outputDir);
235
+ }
236
+ });
237
+ // Safely validate transcription result
238
+ if (!transcription || typeof transcription !== 'object' || typeof transcription.text !== 'string') {
239
+ throw new Error('Invalid transcription result: missing or invalid text property');
240
+ }
241
+ const audioContext = transcription.text;
242
+ if (!audioContext.trim()) {
243
+ logger.warn('AUDIO_REVIEW_NO_TRANSCRIPT: No audio content transcribed | File: %s | Reason: Empty or invalid audio | Action: Skipping', audioFilePath);
244
+ return '';
245
+ } else {
246
+ logger.info('AUDIO_REVIEW_TRANSCRIBED: Successfully transcribed audio | File: %s | Length: %d characters | Status: ready', path.basename(audioFilePath), audioContext.length);
247
+ logger.debug('Transcribed text: %s', audioContext);
248
+ }
249
+ // Now delegate to the regular review command with the audio context
250
+ logger.info('AUDIO_REVIEW_ANALYZING: Analyzing review from transcript | File: %s | Action: AI analysis | Purpose: Extract issues', path.basename(audioFilePath));
251
+ const result = await review({
252
+ ...runConfig,
253
+ review: {
254
+ // Map audioReview configuration to review configuration
255
+ includeCommitHistory: (_runConfig_audioReview = runConfig.audioReview) === null || _runConfig_audioReview === void 0 ? void 0 : _runConfig_audioReview.includeCommitHistory,
256
+ includeRecentDiffs: (_runConfig_audioReview1 = runConfig.audioReview) === null || _runConfig_audioReview1 === void 0 ? void 0 : _runConfig_audioReview1.includeRecentDiffs,
257
+ includeReleaseNotes: (_runConfig_audioReview2 = runConfig.audioReview) === null || _runConfig_audioReview2 === void 0 ? void 0 : _runConfig_audioReview2.includeReleaseNotes,
258
+ includeGithubIssues: (_runConfig_audioReview3 = runConfig.audioReview) === null || _runConfig_audioReview3 === void 0 ? void 0 : _runConfig_audioReview3.includeGithubIssues,
259
+ commitHistoryLimit: (_runConfig_audioReview4 = runConfig.audioReview) === null || _runConfig_audioReview4 === void 0 ? void 0 : _runConfig_audioReview4.commitHistoryLimit,
260
+ diffHistoryLimit: (_runConfig_audioReview5 = runConfig.audioReview) === null || _runConfig_audioReview5 === void 0 ? void 0 : _runConfig_audioReview5.diffHistoryLimit,
261
+ releaseNotesLimit: (_runConfig_audioReview6 = runConfig.audioReview) === null || _runConfig_audioReview6 === void 0 ? void 0 : _runConfig_audioReview6.releaseNotesLimit,
262
+ githubIssuesLimit: (_runConfig_audioReview7 = runConfig.audioReview) === null || _runConfig_audioReview7 === void 0 ? void 0 : _runConfig_audioReview7.githubIssuesLimit,
263
+ sendit: (_runConfig_audioReview8 = runConfig.audioReview) === null || _runConfig_audioReview8 === void 0 ? void 0 : _runConfig_audioReview8.sendit,
264
+ context: (_runConfig_audioReview9 = runConfig.audioReview) === null || _runConfig_audioReview9 === void 0 ? void 0 : _runConfig_audioReview9.context,
265
+ // Use the transcribed audio as content with file context
266
+ note: `Audio Review from ${path.basename(audioFilePath)}:\n\n${audioContext.trim()}`
267
+ }
268
+ });
269
+ return result;
270
+ } catch (error) {
271
+ logger.error('AUDIO_REVIEW_FILE_FAILED: Failed to process audio file | File: %s | Error: %s | Impact: File not analyzed', audioFilePath, error.message);
272
+ return `Failed to process ${path.basename(audioFilePath)}: ${error.message}`;
273
+ }
274
+ };
275
+ const execute = async (runConfig)=>{
276
+ var _runConfig_audioReview, _runConfig_audioReview1, _runConfig_audioReview2, _runConfig_audioReview3, _runConfig_audioReview4, _runConfig_audioReview5, _runConfig_audioReview6, _runConfig_audioReview7, _runConfig_audioReview8, _runConfig_audioReview9, _runConfig_review;
277
+ const isDryRun = runConfig.dryRun || false;
278
+ const logger = getDryRunLogger(isDryRun);
279
+ // Check if directory option is provided with safe access
280
+ const audioReviewConfig = runConfig.audioReview;
281
+ const directory = audioReviewConfig && typeof audioReviewConfig === 'object' && 'directory' in audioReviewConfig ? audioReviewConfig.directory : undefined;
282
+ if (directory) {
283
+ // Directory batch processing mode
284
+ logger.info('AUDIO_REVIEW_BATCH_STARTING: Starting directory batch audio review | Directory: %s | Mode: batch | Purpose: Process all audio files', directory);
285
+ if (isDryRun) {
286
+ logger.info('AUDIO_REVIEW_BATCH_DRY_RUN: Would discover and process audio files | Mode: dry-run | Directory: %s | Action: Discover + transcribe + analyze', directory);
287
+ logger.info('AUDIO_REVIEW_BATCH_WORKFLOW: Would transcribe and analyze each file | Mode: dry-run | Purpose: Review analysis from audio');
288
+ return 'DRY RUN: Directory batch processing would be performed';
289
+ }
290
+ try {
291
+ // Discover audio files in the directory
292
+ const audioFiles = await discoverAudioFiles(directory);
293
+ if (audioFiles.length === 0) {
294
+ logger.warn('AUDIO_REVIEW_NO_FILES: No audio files found in directory | Directory: %s | Extensions: .mp3, .wav, .m4a, .ogg | Action: Nothing to process', directory);
295
+ return 'No audio files found to process';
296
+ }
297
+ const results = [];
298
+ // Process each audio file
299
+ for(let i = 0; i < audioFiles.length; i++){
300
+ const audioFile = audioFiles[i];
301
+ logger.info(`\nAUDIO_REVIEW_BATCH_FILE: Processing batch file | Progress: ${i + 1}/${audioFiles.length} | File: ${path.basename(audioFile)}`);
302
+ const result = await processSingleAudioFile(audioFile, runConfig);
303
+ results.push(`File: ${path.basename(audioFile)}\n${result}`);
304
+ // Add a separator between files (except for the last one)
305
+ if (i < audioFiles.length - 1) {
306
+ logger.info('AUDIO_REVIEW_FILE_COMPLETE: Completed file processing | File: %s | Status: completed\n', path.basename(audioFile));
307
+ }
308
+ }
309
+ logger.info('AUDIO_REVIEW_BATCH_COMPLETE: Completed batch processing | Files Processed: %d | Status: all-completed', audioFiles.length);
310
+ // Combine all results
311
+ const combinedResults = `Batch Audio Review Results (${audioFiles.length} files):\n\n` + results.join('\n\n---\n\n');
312
+ return combinedResults;
313
+ } catch (error) {
314
+ logger.error('AUDIO_REVIEW_BATCH_FAILED: Directory batch processing failed | Error: %s | Impact: Batch incomplete', error.message);
315
+ throw error;
316
+ }
317
+ }
318
+ // Original single file/recording logic
319
+ if (isDryRun) {
320
+ var _runConfig_audioReview10;
321
+ if ((_runConfig_audioReview10 = runConfig.audioReview) === null || _runConfig_audioReview10 === void 0 ? void 0 : _runConfig_audioReview10.file) {
322
+ logger.info('AUDIO_REVIEW_FILE_DRY_RUN: Would process audio file | Mode: dry-run | File: %s | Action: Transcribe + analyze', runConfig.audioReview.file);
323
+ logger.info('AUDIO_REVIEW_WORKFLOW_DRY_RUN: Would transcribe and analyze | Mode: dry-run | Purpose: Review context from audio');
324
+ } else {
325
+ logger.info('AUDIO_REVIEW_RECORD_DRY_RUN: Would start audio recording | Mode: dry-run | Purpose: Review context');
326
+ logger.info('AUDIO_REVIEW_TRANSCRIPT_DRY_RUN: Would transcribe and analyze | Mode: dry-run | Purpose: Extract review content');
327
+ }
328
+ logger.info('AUDIO_REVIEW_DELEGATE_DRY_RUN: Would delegate to regular review command | Mode: dry-run | Next: Standard review flow');
329
+ // Return preview without calling real commands
330
+ return 'DRY RUN: Would process audio, transcribe it, and perform review analysis with audio context';
331
+ }
332
+ let audioContext;
333
+ try {
334
+ var _runConfig_audioReview11, _runConfig_audioReview12, _runConfig_audioReview13, _runConfig_audioReview14;
335
+ // Step 1: Record audio using unplayable with new key handling
336
+ logger.info('AUDIO_REVIEW_RECORDING_STARTING: Starting audio recording | Purpose: Capture review context | Tool: unplayable');
337
+ if (!((_runConfig_audioReview11 = runConfig.audioReview) === null || _runConfig_audioReview11 === void 0 ? void 0 : _runConfig_audioReview11.file)) {
338
+ logger.info('AUDIO_REVIEW_RECORDING_ACTIVE: Recording in progress | Action: Press ENTER to stop | Alternative: Press C to cancel');
339
+ }
340
+ // Start countdown timer if recording (not processing a file) and maxRecordingTime is set
341
+ const maxRecordingTime = (_runConfig_audioReview12 = runConfig.audioReview) === null || _runConfig_audioReview12 === void 0 ? void 0 : _runConfig_audioReview12.maxRecordingTime;
342
+ const isRecording = !((_runConfig_audioReview13 = runConfig.audioReview) === null || _runConfig_audioReview13 === void 0 ? void 0 : _runConfig_audioReview13.file);
343
+ let countdownTimer = null;
344
+ if (isRecording && maxRecordingTime && maxRecordingTime > 0) {
345
+ countdownTimer = createAudioRecordingCountdown(maxRecordingTime);
346
+ // Start countdown timer in parallel with recording
347
+ countdownTimer.start().catch(()=>{
348
+ // Timer completed naturally, no action needed
349
+ });
350
+ }
351
+ let audioResult;
352
+ try {
353
+ var _runConfig_audioReview15, _runConfig_audioReview16;
354
+ // Use processAudio with proper configuration
355
+ audioResult = await processAudio({
356
+ file: (_runConfig_audioReview15 = runConfig.audioReview) === null || _runConfig_audioReview15 === void 0 ? void 0 : _runConfig_audioReview15.file,
357
+ maxRecordingTime: (_runConfig_audioReview16 = runConfig.audioReview) === null || _runConfig_audioReview16 === void 0 ? void 0 : _runConfig_audioReview16.maxRecordingTime,
358
+ outputDirectory: runConfig.outputDirectory || 'output',
359
+ debug: runConfig.debug
360
+ });
361
+ } finally{
362
+ // Stop countdown timer if it was running
363
+ if (countdownTimer) {
364
+ countdownTimer.stop();
365
+ }
366
+ }
367
+ // Check if recording was cancelled
368
+ if (audioResult.cancelled) {
369
+ logger.info('AUDIO_REVIEW_CANCELLED: Audio review cancelled by user | Reason: User choice | Status: aborted');
370
+ throw new CancellationError('Audio review cancelled by user');
371
+ }
372
+ // Step 2: Get the audio file path from the result
373
+ let audioFilePath;
374
+ if ((_runConfig_audioReview14 = runConfig.audioReview) === null || _runConfig_audioReview14 === void 0 ? void 0 : _runConfig_audioReview14.file) {
375
+ // Use the provided file path
376
+ audioFilePath = runConfig.audioReview.file;
377
+ } else if (audioResult.audioFilePath) {
378
+ // Use the file path returned by processAudio
379
+ audioFilePath = audioResult.audioFilePath;
380
+ } else {
381
+ // Fallback to generated filename (this should rarely happen now)
382
+ const outputDir = runConfig.outputDirectory || 'output';
383
+ audioFilePath = path.join(outputDir, getTimestampedAudioFilename());
384
+ logger.warn('AUDIO_REVIEW_FILENAME_GENERATED: Using generated filename for audio | Filename: %s | Warning: May not match actual file from unplayable', audioFilePath);
385
+ logger.warn('AUDIO_REVIEW_FILENAME_NOTE: Filename mismatch possible | Tool: unplayable | Impact: May need manual file lookup');
386
+ }
387
+ // Step 3: Use kodrdriv's transcription functionality
388
+ logger.info('AUDIO_REVIEW_TRANSCRIBING_LOCAL: Transcribing audio locally | Service: OpenAI Whisper | Mode: local | Purpose: Convert speech to text');
389
+ const outputDir = runConfig.outputDirectory || 'output';
390
+ const aiStorageAdapter = createStorageAdapter(outputDir);
391
+ const aiLogger = createLoggerAdapter(isDryRun);
392
+ const transcription = await transcribeAudio(audioFilePath, {
393
+ model: "whisper-1",
394
+ debug: runConfig.debug,
395
+ storage: aiStorageAdapter,
396
+ logger: aiLogger,
397
+ onArchive: async (audioPath, transcriptionText)=>{
398
+ const outputDir = path.join(runConfig.outputDirectory || 'output', 'kodrdriv');
399
+ await archiveAudio(audioPath, transcriptionText, outputDir);
400
+ }
401
+ });
402
+ // Safely validate transcription result
403
+ if (!transcription || typeof transcription !== 'object' || typeof transcription.text !== 'string') {
404
+ throw new Error('Invalid transcription result: missing or invalid text property');
405
+ }
406
+ audioContext = transcription.text;
407
+ if (!audioContext.trim()) {
408
+ logger.warn('AUDIO_REVIEW_NO_CONTENT: No audio content transcribed | Reason: Empty or invalid | Action: Proceeding without audio context');
409
+ audioContext = '';
410
+ } else {
411
+ logger.info('AUDIO_REVIEW_TRANSCRIPT_SUCCESS: Successfully transcribed audio | Tool: kodrdriv | Length: ' + audioContext.length + ' characters | Status: ready');
412
+ logger.debug('Transcribed text: %s', audioContext);
413
+ }
414
+ } catch (error) {
415
+ // Re-throw CancellationError to properly handle cancellation
416
+ if (error.name === 'CancellationError') {
417
+ throw error;
418
+ }
419
+ logger.error('AUDIO_REVIEW_PROCESSING_FAILED: Audio processing failed | Error: %s | Impact: No audio context available', error.message);
420
+ logger.info('AUDIO_REVIEW_FALLBACK: Proceeding without audio context | Mode: fallback | Next: Standard review analysis');
421
+ audioContext = '';
422
+ }
423
+ // Now delegate to the regular review command with the audio context
424
+ logger.info('AUDIO_REVIEW_ANALYSIS_STARTING: Analyzing review with audio context | Source: transcript | Purpose: Extract actionable issues');
425
+ const result = await review({
426
+ ...runConfig,
427
+ review: {
428
+ // Map audioReview configuration to review configuration
429
+ includeCommitHistory: (_runConfig_audioReview = runConfig.audioReview) === null || _runConfig_audioReview === void 0 ? void 0 : _runConfig_audioReview.includeCommitHistory,
430
+ includeRecentDiffs: (_runConfig_audioReview1 = runConfig.audioReview) === null || _runConfig_audioReview1 === void 0 ? void 0 : _runConfig_audioReview1.includeRecentDiffs,
431
+ includeReleaseNotes: (_runConfig_audioReview2 = runConfig.audioReview) === null || _runConfig_audioReview2 === void 0 ? void 0 : _runConfig_audioReview2.includeReleaseNotes,
432
+ includeGithubIssues: (_runConfig_audioReview3 = runConfig.audioReview) === null || _runConfig_audioReview3 === void 0 ? void 0 : _runConfig_audioReview3.includeGithubIssues,
433
+ commitHistoryLimit: (_runConfig_audioReview4 = runConfig.audioReview) === null || _runConfig_audioReview4 === void 0 ? void 0 : _runConfig_audioReview4.commitHistoryLimit,
434
+ diffHistoryLimit: (_runConfig_audioReview5 = runConfig.audioReview) === null || _runConfig_audioReview5 === void 0 ? void 0 : _runConfig_audioReview5.diffHistoryLimit,
435
+ releaseNotesLimit: (_runConfig_audioReview6 = runConfig.audioReview) === null || _runConfig_audioReview6 === void 0 ? void 0 : _runConfig_audioReview6.releaseNotesLimit,
436
+ githubIssuesLimit: (_runConfig_audioReview7 = runConfig.audioReview) === null || _runConfig_audioReview7 === void 0 ? void 0 : _runConfig_audioReview7.githubIssuesLimit,
437
+ sendit: (_runConfig_audioReview8 = runConfig.audioReview) === null || _runConfig_audioReview8 === void 0 ? void 0 : _runConfig_audioReview8.sendit,
438
+ context: (_runConfig_audioReview9 = runConfig.audioReview) === null || _runConfig_audioReview9 === void 0 ? void 0 : _runConfig_audioReview9.context,
439
+ // Use the transcribed audio as content
440
+ note: audioContext.trim() || ((_runConfig_review = runConfig.review) === null || _runConfig_review === void 0 ? void 0 : _runConfig_review.note) || ''
441
+ }
442
+ });
443
+ return result;
444
+ };
445
+
446
+ export { execute$1 as audioCommit, execute as audioReview, execute$2 as selectAudio };
447
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/commands/select-audio.ts","../src/commands/audio-commit.ts","../src/commands/audio-review.ts"],"sourcesContent":["#!/usr/bin/env node\nimport path from 'path';\nimport os from 'os';\nimport { getDryRunLogger, Config } from '@grunnverk/core';\nimport { selectAndConfigureAudioDevice } from '@theunwalked/unplayable';\n\nconst getUnplayableConfigPath = (): string => {\n try {\n return path.join(os.homedir(), '.unplayable', 'audio-device.json');\n } catch (error: any) {\n throw new Error(`Failed to determine home directory: ${error.message}`);\n }\n};\n\nexport const execute = async (runConfig: Config): Promise<string> => {\n const isDryRun = runConfig.dryRun || false;\n const logger = getDryRunLogger(isDryRun);\n\n if (isDryRun) {\n try {\n const configPath = getUnplayableConfigPath();\n logger.info('AUDIO_SELECT_DRY_RUN: Would start audio device selection | Mode: dry-run | Purpose: Choose input device');\n logger.info('AUDIO_SELECT_SAVE_DRY_RUN: Would save device to config | Mode: dry-run | Path: %s', configPath);\n return 'Audio device selection completed (dry run)';\n } catch (error: any) {\n logger.warn('AUDIO_SELECT_CONFIG_PATH_ERROR: Error determining config path | Error: %s | Impact: Cannot show save location', error.message);\n return 'Audio device selection completed (dry run)';\n }\n }\n\n try {\n const preferencesDir = path.join(os.homedir(), '.unplayable');\n const result = await selectAndConfigureAudioDevice(preferencesDir, logger, runConfig.debug);\n return result;\n } catch (error: any) {\n // Check if this is a home directory error\n if (error.message && error.message.includes('Failed to determine home directory')) {\n logger.error('AUDIO_SELECT_FAILED: Audio device selection failed | Error: %s', error.message);\n throw new Error(`Failed to determine home directory: ${error.message}`);\n } else {\n const errorMessage = error.message || error.toString();\n logger.error('AUDIO_SELECT_COMMAND_FAILED: Audio device selection command failed | Error: %s | Status: failed', errorMessage);\n throw new Error(`Audio device selection failed: ${errorMessage}`);\n }\n }\n};\n\n","#!/usr/bin/env node\nimport path from 'path';\nimport { processAudio } from '@theunwalked/unplayable';\nimport { CancellationError, UserCancellationError } from '@grunnverk/shared';\nimport { getDryRunLogger, getLogger, Config, getTimestampedAudioFilename, createStorageAdapter, createLoggerAdapter } from '@grunnverk/core';\nimport { transcribeAudio } from '@grunnverk/ai-service';\nimport { commit as executeCommit } from '@grunnverk/commands-git';\nimport { createAudioRecordingCountdown, archiveAudio } from '@grunnverk/audio-tools';\n\nconst executeInternal = async (runConfig: Config): Promise<string> => {\n const isDryRun = runConfig.dryRun || false;\n const logger = getDryRunLogger(isDryRun);\n\n if (isDryRun) {\n if (runConfig.audioCommit?.file) {\n logger.info('AUDIO_COMMIT_FILE_DRY_RUN: Would process audio file | Mode: dry-run | File: %s | Action: Transcribe + generate commit', runConfig.audioCommit.file);\n logger.info('AUDIO_COMMIT_WORKFLOW_DRY_RUN: Would transcribe and generate message | Mode: dry-run | Purpose: Commit message from audio');\n } else {\n logger.info('AUDIO_COMMIT_RECORD_DRY_RUN: Would start audio recording | Mode: dry-run | Purpose: Commit context');\n logger.info('AUDIO_COMMIT_TRANSCRIPT_DRY_RUN: Would transcribe and generate | Mode: dry-run | Purpose: Extract commit message');\n }\n logger.info('AUDIO_COMMIT_DELEGATE_DRY_RUN: Would delegate to regular commit command | Mode: dry-run | Next: Standard commit flow');\n\n // Return preview without calling real commands\n return 'DRY RUN: Would process audio, transcribe it, and generate commit message with audio context';\n }\n\n let audioContext: string;\n\n try {\n // Step 1: Record audio using unplayable with new key handling\n logger.info('AUDIO_COMMIT_RECORDING_STARTING: Starting audio recording | Purpose: Capture commit context | Tool: unplayable');\n\n if (!runConfig.audioCommit?.file) {\n logger.info('AUDIO_COMMIT_RECORDING_ACTIVE: Recording in progress | Action: Press ENTER to stop | Alternative: Press C to cancel');\n }\n\n // Start countdown timer if recording (not processing a file) and maxRecordingTime is set\n const maxRecordingTime = runConfig.audioCommit?.maxRecordingTime;\n const isRecording = !runConfig.audioCommit?.file;\n let countdownTimer: ReturnType<typeof createAudioRecordingCountdown> | null = null;\n\n if (isRecording && maxRecordingTime && maxRecordingTime > 0) {\n countdownTimer = createAudioRecordingCountdown(maxRecordingTime);\n // Start countdown timer in parallel with recording\n countdownTimer.start().catch(() => {\n // Timer completed naturally, no action needed\n });\n }\n\n let audioResult: any;\n try {\n // Use processAudio with proper configuration\n audioResult = await processAudio({\n file: runConfig.audioCommit?.file,\n maxRecordingTime: runConfig.audioCommit?.maxRecordingTime,\n outputDirectory: runConfig.outputDirectory || 'output',\n debug: runConfig.debug\n });\n } finally {\n // Stop countdown timer if it was running\n if (countdownTimer) {\n countdownTimer.stop();\n }\n }\n\n // Check if recording was cancelled\n if (audioResult.cancelled) {\n logger.info('AUDIO_COMMIT_CANCELLED: Audio commit cancelled by user | Reason: User choice | Status: aborted');\n throw new UserCancellationError('Audio commit cancelled by user');\n }\n\n // Step 2: Get the audio file path from the result\n let audioFilePath: string;\n\n if (runConfig.audioCommit?.file) {\n // Use the provided file path\n audioFilePath = runConfig.audioCommit.file;\n } else if (audioResult.audioFilePath) {\n // Use the file path returned by processAudio\n audioFilePath = audioResult.audioFilePath;\n } else {\n // Fallback to generated filename (this should rarely happen now)\n const outputDir = runConfig.outputDirectory || 'output';\n audioFilePath = path.join(outputDir, getTimestampedAudioFilename());\n logger.warn('AUDIO_COMMIT_FILENAME_GENERATED: Using generated filename for audio | Filename: %s | Warning: May not match actual file from unplayable', audioFilePath);\n logger.warn('AUDIO_COMMIT_FILENAME_NOTE: Filename mismatch possible | Tool: unplayable | Impact: May need manual file lookup');\n }\n\n // Step 3: Use ai-service transcription functionality\n logger.info('AUDIO_COMMIT_TRANSCRIBING: Transcribing audio locally | Service: OpenAI Whisper | Mode: local | Purpose: Convert speech to text');\n\n const outputDir = runConfig.outputDirectory || 'output';\n const aiStorageAdapter = createStorageAdapter(outputDir);\n const aiLogger = createLoggerAdapter(isDryRun);\n\n const transcription = await transcribeAudio(audioFilePath, {\n model: \"whisper-1\",\n debug: runConfig.debug,\n storage: aiStorageAdapter,\n logger: aiLogger,\n onArchive: async (audioPath: string, transcriptionText: string) => {\n const outputDir = path.join(runConfig.outputDirectory || 'output', 'kodrdriv');\n await archiveAudio(audioPath, transcriptionText, outputDir);\n },\n });\n\n audioContext = transcription.text;\n\n if (!audioContext.trim()) {\n logger.warn('AUDIO_COMMIT_NO_CONTENT: No audio content transcribed | Reason: Empty or invalid | Action: Proceeding without audio context');\n audioContext = '';\n } else {\n logger.info('AUDIO_COMMIT_TRANSCRIPT_SUCCESS: Successfully transcribed audio | Tool: kodrdriv | Length: ' + audioContext.length + ' characters | Status: ready');\n logger.debug('Transcribed text: %s', audioContext);\n }\n\n } catch (error: any) {\n // Re-throw cancellation errors properly\n if (error instanceof UserCancellationError) {\n throw error;\n }\n\n // Convert old CancellationError to new UserCancellationError\n if (error.name === 'CancellationError' || error instanceof CancellationError) {\n throw new UserCancellationError(error.message);\n }\n\n logger.error('AUDIO_COMMIT_PROCESSING_FAILED: Audio processing failed | Error: %s | Impact: No audio context available', error.message);\n logger.info('AUDIO_COMMIT_FALLBACK: Proceeding without audio context | Mode: fallback | Next: Standard commit generation');\n audioContext = '';\n }\n\n // Now delegate to the regular commit command with the audio context\n logger.info('AUDIO_COMMIT_GENERATING: Generating commit message with audio context | Source: transcript | Purpose: AI-generated commit message');\n const result = await executeCommit({\n ...runConfig,\n commit: {\n ...runConfig.commit,\n direction: audioContext.trim() || runConfig.commit?.direction || ''\n }\n });\n\n return result;\n};\n\nexport const execute = async (runConfig: Config): Promise<string> => {\n try {\n return await executeInternal(runConfig);\n } catch (error: any) {\n const logger = getLogger();\n\n // Handle user cancellation gracefully - don't exit process\n if (error instanceof UserCancellationError) {\n logger.info('AUDIO_COMMIT_ERROR: Error during audio commit | Error: ' + error.message);\n throw error; // Let calling code handle this\n }\n\n // Handle other errors - don't exit process\n logger.error(`AUDIO_COMMIT_FAILED: Audio commit command failed | Error: ${error.message} | Impact: Commit not generated`);\n if (error.cause) {\n logger.debug(`Caused by: ${error.cause.message}`);\n }\n throw error; // Let calling code handle this\n }\n};\n\n","#!/usr/bin/env node\n\nimport { getLogger, getDryRunLogger, Config, getTimestampedAudioFilename, createStorageAdapter, createLoggerAdapter } from '@grunnverk/core';\nimport { review as executeReview } from '@grunnverk/commands-git';\nimport { processAudio } from '@theunwalked/unplayable';\nimport { transcribeAudio } from '@grunnverk/ai-service';\nimport { CancellationError, createStorage } from '@grunnverk/shared';\nimport { createAudioRecordingCountdown, archiveAudio } from '@grunnverk/audio-tools';\nimport path from 'path';\n\n// Common audio file extensions\nconst AUDIO_EXTENSIONS = ['.wav', '.mp3', '.m4a', '.aac', '.flac', '.ogg', '.wma'];\n\n/**\n * Discover audio files in a directory\n */\nconst discoverAudioFiles = async (directory: string): Promise<string[]> => {\n const logger = getLogger();\n const storage = createStorage();\n\n try {\n if (!(await storage.isDirectoryReadable(directory))) {\n throw new Error(`Directory not readable: ${directory}`);\n }\n\n const allFiles = await storage.listFiles(directory);\n const audioFiles = allFiles\n .filter(file => AUDIO_EXTENSIONS.includes(path.extname(file).toLowerCase()))\n .map(file => path.join(directory, file))\n .sort(); // Sort for consistent processing order\n\n logger.info(`AUDIO_REVIEW_FILES_FOUND: Found audio files in directory | Count: ${audioFiles.length} | Directory: ${directory} | Status: ready-for-processing`);\n logger.debug('Audio files found: %s', audioFiles.join(', '));\n\n return audioFiles;\n } catch (error: any) {\n logger.error('AUDIO_REVIEW_DISCOVERY_FAILED: Failed to discover audio files | Directory: %s | Error: %s | Impact: Cannot process batch', directory, error.message);\n throw error;\n }\n};\n\n/**\n * Process a single audio file for review\n */\nconst processSingleAudioFile = async (audioFilePath: string, runConfig: Config): Promise<string> => {\n const logger = getLogger();\n\n try {\n logger.info('AUDIO_REVIEW_PROCESSING: Processing audio file | File: %s | Action: Transcribe and analyze', path.basename(audioFilePath));\n\n // Use kodrdriv's transcription functionality\n logger.info('AUDIO_REVIEW_TRANSCRIBING: Transcribing audio using OpenAI Whisper | Service: OpenAI Whisper | Purpose: Convert speech to text');\n\n const outputDir = runConfig.outputDirectory || 'output';\n const aiStorageAdapter = createStorageAdapter(outputDir);\n const aiLogger = createLoggerAdapter(runConfig.dryRun || false);\n\n const transcription = await transcribeAudio(audioFilePath, {\n model: \"whisper-1\",\n debug: runConfig.debug,\n storage: aiStorageAdapter,\n logger: aiLogger,\n onArchive: async (audioPath: string, transcriptionText: string) => {\n const outputDir = path.join(runConfig.outputDirectory || 'output', 'kodrdriv');\n await archiveAudio(audioPath, transcriptionText, outputDir);\n },\n });\n\n // Safely validate transcription result\n if (!transcription || typeof transcription !== 'object' || typeof transcription.text !== 'string') {\n throw new Error('Invalid transcription result: missing or invalid text property');\n }\n const audioContext = transcription.text;\n\n if (!audioContext.trim()) {\n logger.warn('AUDIO_REVIEW_NO_TRANSCRIPT: No audio content transcribed | File: %s | Reason: Empty or invalid audio | Action: Skipping', audioFilePath);\n return '';\n } else {\n logger.info('AUDIO_REVIEW_TRANSCRIBED: Successfully transcribed audio | File: %s | Length: %d characters | Status: ready', path.basename(audioFilePath), audioContext.length);\n logger.debug('Transcribed text: %s', audioContext);\n }\n\n // Now delegate to the regular review command with the audio context\n logger.info('AUDIO_REVIEW_ANALYZING: Analyzing review from transcript | File: %s | Action: AI analysis | Purpose: Extract issues', path.basename(audioFilePath));\n const result = await executeReview({\n ...runConfig,\n review: {\n // Map audioReview configuration to review configuration\n includeCommitHistory: runConfig.audioReview?.includeCommitHistory,\n includeRecentDiffs: runConfig.audioReview?.includeRecentDiffs,\n includeReleaseNotes: runConfig.audioReview?.includeReleaseNotes,\n includeGithubIssues: runConfig.audioReview?.includeGithubIssues,\n commitHistoryLimit: runConfig.audioReview?.commitHistoryLimit,\n diffHistoryLimit: runConfig.audioReview?.diffHistoryLimit,\n releaseNotesLimit: runConfig.audioReview?.releaseNotesLimit,\n githubIssuesLimit: runConfig.audioReview?.githubIssuesLimit,\n sendit: runConfig.audioReview?.sendit,\n context: runConfig.audioReview?.context,\n // Use the transcribed audio as content with file context\n note: `Audio Review from ${path.basename(audioFilePath)}:\\n\\n${audioContext.trim()}`\n }\n });\n\n return result;\n\n } catch (error: any) {\n logger.error('AUDIO_REVIEW_FILE_FAILED: Failed to process audio file | File: %s | Error: %s | Impact: File not analyzed', audioFilePath, error.message);\n return `Failed to process ${path.basename(audioFilePath)}: ${error.message}`;\n }\n};\n\nexport const execute = async (runConfig: Config): Promise<string> => {\n const isDryRun = runConfig.dryRun || false;\n const logger = getDryRunLogger(isDryRun);\n\n // Check if directory option is provided with safe access\n const audioReviewConfig = runConfig.audioReview;\n const directory = audioReviewConfig && typeof audioReviewConfig === 'object' && 'directory' in audioReviewConfig\n ? (audioReviewConfig as any).directory\n : undefined;\n\n if (directory) {\n // Directory batch processing mode\n logger.info('AUDIO_REVIEW_BATCH_STARTING: Starting directory batch audio review | Directory: %s | Mode: batch | Purpose: Process all audio files', directory);\n\n if (isDryRun) {\n logger.info('AUDIO_REVIEW_BATCH_DRY_RUN: Would discover and process audio files | Mode: dry-run | Directory: %s | Action: Discover + transcribe + analyze', directory);\n logger.info('AUDIO_REVIEW_BATCH_WORKFLOW: Would transcribe and analyze each file | Mode: dry-run | Purpose: Review analysis from audio');\n return 'DRY RUN: Directory batch processing would be performed';\n }\n\n try {\n // Discover audio files in the directory\n const audioFiles = await discoverAudioFiles(directory);\n\n if (audioFiles.length === 0) {\n logger.warn('AUDIO_REVIEW_NO_FILES: No audio files found in directory | Directory: %s | Extensions: .mp3, .wav, .m4a, .ogg | Action: Nothing to process', directory);\n return 'No audio files found to process';\n }\n\n const results: string[] = [];\n\n // Process each audio file\n for (let i = 0; i < audioFiles.length; i++) {\n const audioFile = audioFiles[i];\n logger.info(`\\nAUDIO_REVIEW_BATCH_FILE: Processing batch file | Progress: ${i + 1}/${audioFiles.length} | File: ${path.basename(audioFile)}`);\n\n const result = await processSingleAudioFile(audioFile, runConfig);\n results.push(`File: ${path.basename(audioFile)}\\n${result}`);\n\n // Add a separator between files (except for the last one)\n if (i < audioFiles.length - 1) {\n logger.info('AUDIO_REVIEW_FILE_COMPLETE: Completed file processing | File: %s | Status: completed\\n', path.basename(audioFile));\n }\n }\n\n logger.info('AUDIO_REVIEW_BATCH_COMPLETE: Completed batch processing | Files Processed: %d | Status: all-completed', audioFiles.length);\n\n // Combine all results\n const combinedResults = `Batch Audio Review Results (${audioFiles.length} files):\\n\\n` +\n results.join('\\n\\n---\\n\\n');\n\n return combinedResults;\n\n } catch (error: any) {\n logger.error('AUDIO_REVIEW_BATCH_FAILED: Directory batch processing failed | Error: %s | Impact: Batch incomplete', error.message);\n throw error;\n }\n }\n\n // Original single file/recording logic\n if (isDryRun) {\n if (runConfig.audioReview?.file) {\n logger.info('AUDIO_REVIEW_FILE_DRY_RUN: Would process audio file | Mode: dry-run | File: %s | Action: Transcribe + analyze', runConfig.audioReview.file);\n logger.info('AUDIO_REVIEW_WORKFLOW_DRY_RUN: Would transcribe and analyze | Mode: dry-run | Purpose: Review context from audio');\n } else {\n logger.info('AUDIO_REVIEW_RECORD_DRY_RUN: Would start audio recording | Mode: dry-run | Purpose: Review context');\n logger.info('AUDIO_REVIEW_TRANSCRIPT_DRY_RUN: Would transcribe and analyze | Mode: dry-run | Purpose: Extract review content');\n }\n logger.info('AUDIO_REVIEW_DELEGATE_DRY_RUN: Would delegate to regular review command | Mode: dry-run | Next: Standard review flow');\n\n // Return preview without calling real commands\n return 'DRY RUN: Would process audio, transcribe it, and perform review analysis with audio context';\n }\n\n let audioContext: string;\n\n try {\n // Step 1: Record audio using unplayable with new key handling\n logger.info('AUDIO_REVIEW_RECORDING_STARTING: Starting audio recording | Purpose: Capture review context | Tool: unplayable');\n\n if (!runConfig.audioReview?.file) {\n logger.info('AUDIO_REVIEW_RECORDING_ACTIVE: Recording in progress | Action: Press ENTER to stop | Alternative: Press C to cancel');\n }\n\n // Start countdown timer if recording (not processing a file) and maxRecordingTime is set\n const maxRecordingTime = runConfig.audioReview?.maxRecordingTime;\n const isRecording = !runConfig.audioReview?.file;\n let countdownTimer: ReturnType<typeof createAudioRecordingCountdown> | null = null;\n\n if (isRecording && maxRecordingTime && maxRecordingTime > 0) {\n countdownTimer = createAudioRecordingCountdown(maxRecordingTime);\n // Start countdown timer in parallel with recording\n countdownTimer.start().catch(() => {\n // Timer completed naturally, no action needed\n });\n }\n\n let audioResult: any;\n try {\n // Use processAudio with proper configuration\n audioResult = await processAudio({\n file: runConfig.audioReview?.file,\n maxRecordingTime: runConfig.audioReview?.maxRecordingTime,\n outputDirectory: runConfig.outputDirectory || 'output',\n debug: runConfig.debug\n });\n } finally {\n // Stop countdown timer if it was running\n if (countdownTimer) {\n countdownTimer.stop();\n }\n }\n\n // Check if recording was cancelled\n if (audioResult.cancelled) {\n logger.info('AUDIO_REVIEW_CANCELLED: Audio review cancelled by user | Reason: User choice | Status: aborted');\n throw new CancellationError('Audio review cancelled by user');\n }\n\n // Step 2: Get the audio file path from the result\n let audioFilePath: string;\n\n if (runConfig.audioReview?.file) {\n // Use the provided file path\n audioFilePath = runConfig.audioReview.file;\n } else if (audioResult.audioFilePath) {\n // Use the file path returned by processAudio\n audioFilePath = audioResult.audioFilePath;\n } else {\n // Fallback to generated filename (this should rarely happen now)\n const outputDir = runConfig.outputDirectory || 'output';\n audioFilePath = path.join(outputDir, getTimestampedAudioFilename());\n logger.warn('AUDIO_REVIEW_FILENAME_GENERATED: Using generated filename for audio | Filename: %s | Warning: May not match actual file from unplayable', audioFilePath);\n logger.warn('AUDIO_REVIEW_FILENAME_NOTE: Filename mismatch possible | Tool: unplayable | Impact: May need manual file lookup');\n }\n\n // Step 3: Use kodrdriv's transcription functionality\n logger.info('AUDIO_REVIEW_TRANSCRIBING_LOCAL: Transcribing audio locally | Service: OpenAI Whisper | Mode: local | Purpose: Convert speech to text');\n\n const outputDir = runConfig.outputDirectory || 'output';\n const aiStorageAdapter = createStorageAdapter(outputDir);\n const aiLogger = createLoggerAdapter(isDryRun);\n\n const transcription = await transcribeAudio(audioFilePath, {\n model: \"whisper-1\",\n debug: runConfig.debug,\n storage: aiStorageAdapter,\n logger: aiLogger,\n onArchive: async (audioPath: string, transcriptionText: string) => {\n const outputDir = path.join(runConfig.outputDirectory || 'output', 'kodrdriv');\n await archiveAudio(audioPath, transcriptionText, outputDir);\n },\n });\n\n // Safely validate transcription result\n if (!transcription || typeof transcription !== 'object' || typeof transcription.text !== 'string') {\n throw new Error('Invalid transcription result: missing or invalid text property');\n }\n audioContext = transcription.text;\n\n if (!audioContext.trim()) {\n logger.warn('AUDIO_REVIEW_NO_CONTENT: No audio content transcribed | Reason: Empty or invalid | Action: Proceeding without audio context');\n audioContext = '';\n } else {\n logger.info('AUDIO_REVIEW_TRANSCRIPT_SUCCESS: Successfully transcribed audio | Tool: kodrdriv | Length: ' + audioContext.length + ' characters | Status: ready');\n logger.debug('Transcribed text: %s', audioContext);\n }\n\n } catch (error: any) {\n // Re-throw CancellationError to properly handle cancellation\n if (error.name === 'CancellationError') {\n throw error;\n }\n\n logger.error('AUDIO_REVIEW_PROCESSING_FAILED: Audio processing failed | Error: %s | Impact: No audio context available', error.message);\n logger.info('AUDIO_REVIEW_FALLBACK: Proceeding without audio context | Mode: fallback | Next: Standard review analysis');\n audioContext = '';\n }\n\n // Now delegate to the regular review command with the audio context\n logger.info('AUDIO_REVIEW_ANALYSIS_STARTING: Analyzing review with audio context | Source: transcript | Purpose: Extract actionable issues');\n const result = await executeReview({\n ...runConfig,\n review: {\n // Map audioReview configuration to review configuration\n includeCommitHistory: runConfig.audioReview?.includeCommitHistory,\n includeRecentDiffs: runConfig.audioReview?.includeRecentDiffs,\n includeReleaseNotes: runConfig.audioReview?.includeReleaseNotes,\n includeGithubIssues: runConfig.audioReview?.includeGithubIssues,\n commitHistoryLimit: runConfig.audioReview?.commitHistoryLimit,\n diffHistoryLimit: runConfig.audioReview?.diffHistoryLimit,\n releaseNotesLimit: runConfig.audioReview?.releaseNotesLimit,\n githubIssuesLimit: runConfig.audioReview?.githubIssuesLimit,\n sendit: runConfig.audioReview?.sendit,\n context: runConfig.audioReview?.context,\n // Use the transcribed audio as content\n note: audioContext.trim() || runConfig.review?.note || ''\n }\n });\n\n return result;\n};\n\n"],"names":["getUnplayableConfigPath","path","join","os","homedir","error","Error","message","execute","runConfig","isDryRun","dryRun","logger","getDryRunLogger","configPath","info","warn","preferencesDir","result","selectAndConfigureAudioDevice","debug","includes","errorMessage","toString","executeInternal","audioCommit","file","audioContext","maxRecordingTime","isRecording","countdownTimer","createAudioRecordingCountdown","start","catch","audioResult","processAudio","outputDirectory","stop","cancelled","UserCancellationError","audioFilePath","outputDir","getTimestampedAudioFilename","aiStorageAdapter","createStorageAdapter","aiLogger","createLoggerAdapter","transcription","transcribeAudio","model","storage","onArchive","audioPath","transcriptionText","archiveAudio","text","trim","length","name","CancellationError","executeCommit","commit","direction","getLogger","cause","AUDIO_EXTENSIONS","discoverAudioFiles","directory","createStorage","isDirectoryReadable","allFiles","listFiles","audioFiles","filter","extname","toLowerCase","map","sort","processSingleAudioFile","basename","executeReview","review","includeCommitHistory","audioReview","includeRecentDiffs","includeReleaseNotes","includeGithubIssues","commitHistoryLimit","diffHistoryLimit","releaseNotesLimit","githubIssuesLimit","sendit","context","note","audioReviewConfig","undefined","results","i","audioFile","push","combinedResults"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { Config } from '@grunnverk/core';
2
+ export declare const execute: (runConfig: Config) => Promise<string>;
3
+ //# sourceMappingURL=audio-commit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-commit.d.ts","sourceRoot":"","sources":["../../../src/commands/audio-commit.ts"],"names":[],"mappings":";AAIA,OAAO,EAA8B,MAAM,EAA0E,MAAM,iBAAiB,CAAC;AA8I7I,eAAO,MAAM,OAAO,GAAU,WAAW,MAAM,KAAG,OAAO,CAAC,MAAM,CAmB/D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Config } from '@grunnverk/core';
2
+ export declare const execute: (runConfig: Config) => Promise<string>;
3
+ //# sourceMappingURL=audio-review.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-review.d.ts","sourceRoot":"","sources":["../../../src/commands/audio-review.ts"],"names":[],"mappings":";AAEA,OAAO,EAA8B,MAAM,EAA0E,MAAM,iBAAiB,CAAC;AA6G7I,eAAO,MAAM,OAAO,GAAU,WAAW,MAAM,KAAG,OAAO,CAAC,MAAM,CAyM/D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Config } from '@grunnverk/core';
2
+ export declare const execute: (runConfig: Config) => Promise<string>;
3
+ //# sourceMappingURL=select-audio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select-audio.d.ts","sourceRoot":"","sources":["../../../src/commands/select-audio.ts"],"names":[],"mappings":";AAGA,OAAO,EAAmB,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAW1D,eAAO,MAAM,OAAO,GAAU,WAAW,MAAM,KAAG,OAAO,CAAC,MAAM,CA+B/D,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { execute as selectAudio } from './commands/select-audio';
2
+ export { execute as audioCommit } from './commands/audio-commit';
3
+ export { execute as audioReview } from './commands/audio-review';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,yBAAyB,CAAC"}
package/guide/index.md ADDED
@@ -0,0 +1,56 @@
1
+ # @grunnverk/commands-audio - Agentic Guide
2
+
3
+ ## Purpose
4
+
5
+ Audio transcription and voice commands for kodrdriv. Provides:
6
+ - Audio device selection and configuration
7
+ - Audio-based commit message generation
8
+ - Audio-enhanced code review
9
+
10
+ ## Quick Reference
11
+
12
+ For AI agents working with this package:
13
+ - select-audio: Configure audio input device
14
+ - audio-commit: Record/transcribe for commit messages
15
+ - audio-review: Record/transcribe for review context
16
+
17
+ ## Key Exports
18
+
19
+ ```typescript
20
+ // Audio commands
21
+ import { selectAudio, audioCommit, audioReview } from '@grunnverk/commands-audio';
22
+
23
+ // Execute commands
24
+ await selectAudio(config);
25
+ await audioCommit(config);
26
+ await audioReview(config);
27
+ ```
28
+
29
+ ## Dependencies
30
+
31
+ - @grunnverk/core - Core utilities
32
+ - @grunnverk/audio-tools - Audio processing
33
+ - @grunnverk/ai-service - Transcription
34
+ - @theunwalked/unplayable - Audio capture
35
+
36
+ ## Command Workflows
37
+
38
+ ### select-audio
39
+
40
+ 1. List available audio devices
41
+ 2. Prompt user to select device
42
+ 3. Save configuration
43
+
44
+ ### audio-commit
45
+
46
+ 1. Record audio (or use provided file)
47
+ 2. Transcribe using AI
48
+ 3. Generate commit message
49
+ 4. Delegate to commit command
50
+
51
+ ### audio-review
52
+
53
+ 1. Record audio (or use provided files)
54
+ 2. Transcribe using AI
55
+ 3. Add context to review workflow
56
+
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@grunnverk/commands-audio",
3
+ "version": "1.0.0",
4
+ "description": "Audio transcription and voice commands for kodrdriv (transcribe, voice-note)",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "guide"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/grunnverk/commands-audio.git"
20
+ },
21
+ "scripts": {
22
+ "build": "npm run lint && tsc --noEmit && vite build",
23
+ "dev": "vite",
24
+ "watch": "vite build --watch",
25
+ "test": "vitest run --coverage",
26
+ "lint": "eslint . --ext .ts",
27
+ "lint:fix": "eslint . --ext .ts --fix",
28
+ "clean": "rm -rf dist",
29
+ "precommit": "npm run clean && npm run build && npm run lint && npm run test",
30
+ "prepublishOnly": "npm run clean && npm run lint && npm run build && npm run test"
31
+ },
32
+ "keywords": [
33
+ "kodrdriv",
34
+ "audio",
35
+ "transcription",
36
+ "voice",
37
+ "commands"
38
+ ],
39
+ "author": "Tim O'Brien <tobrien@discursive.com>",
40
+ "license": "Apache-2.0",
41
+ "engines": {
42
+ "node": ">=24.0.0"
43
+ },
44
+ "dependencies": {
45
+ "@grunnverk/ai-service": "^1.0.1",
46
+ "@grunnverk/audio-tools": "^1.0.0",
47
+ "@grunnverk/commands-git": "^1.0.0",
48
+ "@grunnverk/core": "^1.0.0",
49
+ "@grunnverk/git-tools": "^1.0.1",
50
+ "@grunnverk/github-tools": "^1.0.0",
51
+ "@grunnverk/shared": "^0.1.11",
52
+ "@riotprompt/riotprompt": "^0.0.21",
53
+ "@theunwalked/unplayable": "^0.0.28",
54
+ "dotenv": "^17.2.1"
55
+ },
56
+ "devDependencies": {
57
+ "@eslint/eslintrc": "^3.3.1",
58
+ "@eslint/js": "^9.33.0",
59
+ "@swc/core": "^1.13.3",
60
+ "@types/node": "^25.0.3",
61
+ "@typescript-eslint/eslint-plugin": "^8.39.1",
62
+ "@typescript-eslint/parser": "^8.47.0",
63
+ "@vitest/coverage-v8": "^4.0.13",
64
+ "ajv": "^8.17.1",
65
+ "esbuild": "0.27.2",
66
+ "eslint": "^9.33.0",
67
+ "eslint-plugin-import": "^2.32.0",
68
+ "globals": "^16.3.0",
69
+ "mockdate": "^3.0.5",
70
+ "rollup": "^4.30.1",
71
+ "typescript": "^5.9.2",
72
+ "vite": "^7.2.4",
73
+ "vite-plugin-dts": "^4.3.0",
74
+ "vite-plugin-node": "^7.0.0",
75
+ "vitest": "^4.0.13"
76
+ },
77
+ "optionalDependencies": {
78
+ "@rollup/rollup-linux-x64-gnu": "^4.30.1",
79
+ "@swc/core-linux-x64-gnu": "^1.13.3"
80
+ }
81
+ }