1
1
import Foundation
2
2
import ArgumentParser
3
+ import AVFoundation
4
+ import AVFAudio
5
+ import Speech
6
+
7
+ enum VibeCommitError : Error , LocalizedError {
8
+ case fileNotFound( String )
9
+ case aiExecutionFailed( Int , String )
10
+ case invalidOutput
11
+ case permissionDenied( String )
12
+
13
+ var errorDescription : String ? {
14
+ switch self {
15
+ case . fileNotFound( let details) :
16
+ return " File not found: \( details) "
17
+ case . aiExecutionFailed( let code, let details) :
18
+ return " AI execution failed with code \( code) : \( details) "
19
+ case . invalidOutput:
20
+ return " Invalid AI output "
21
+ case . permissionDenied( let details) :
22
+ return " Permission denied: \( details) "
23
+ }
24
+ }
25
+ }
3
26
4
27
@main
5
28
struct VibeCommit : ParsableCommand {
@@ -13,17 +36,44 @@ struct VibeCommit: ParsableCommand {
13
36
@Flag ( name: . shortAndLong, help: " Automatically commit using the generated summary. " )
14
37
var autoCommit : Bool = false
15
38
39
+ @Flag ( name: . shortAndLong, help: " Use mock summary instead of real AI (for testing). " )
40
+ var mock : Bool = false
41
+
42
+ @Flag ( name: . shortAndLong, help: " Use voice dictation to input a vibe for the summary. " )
43
+ var voice : Bool = false
44
+
16
45
mutating func run( ) throws {
17
46
guard isGitRepo ( ) else {
18
47
print ( " Error: Not a Git repository. " )
19
48
return
20
49
}
21
50
22
51
if summarize {
23
- let commits = try getGitLog ( )
24
- let summary = try aiSummarize ( commits: commits)
25
- print ( " AI Summary: \n \( summary) " )
26
- try vibeCheck ( summary: summary)
52
+ do {
53
+ let commits = try getGitLog ( )
54
+ var vibe = " "
55
+ if voice {
56
+ let analyzer = SpeechAnalyzer ( )
57
+ vibe = try analyzer. dictate ( )
58
+ print ( " Transcribed vibe: \( vibe) " )
59
+ }
60
+ let summary = try aiSummarize ( commits: commits, vibe: vibe)
61
+ print ( " AI Summary: \n \( summary) " )
62
+ try vibeCheck ( summary: summary)
63
+ } catch let error as VibeCommitError {
64
+ print ( " Error: \( error. localizedDescription) " )
65
+ switch error {
66
+ case . fileNotFound( let details) ,
67
+ . permissionDenied( let details) :
68
+ print ( " Details: \( details) " )
69
+ case . aiExecutionFailed( _, let details) :
70
+ print ( " Details: \( details) " )
71
+ case . invalidOutput:
72
+ print ( " Details: Invalid AI output. " )
73
+ }
74
+ } catch {
75
+ print ( " Unexpected error: \( error. localizedDescription) " )
76
+ }
27
77
} else {
28
78
print ( " Run with --summarize to get started. " )
29
79
}
@@ -49,12 +99,23 @@ struct VibeCommit: ParsableCommand {
49
99
return output
50
100
}
51
101
52
- // AI summary: Bridge to Python/HF for gpt-oss-20b
53
- func aiSummarize( commits: String ) throws -> String {
54
- let scriptPath = " summarize.py " // Assume in project root or adjust path
102
+ // AI summary: Bridge to Python/HF for gpt-oss-20b, or mock, with optional vibe
103
+ func aiSummarize( commits: String , vibe: String ) throws -> String {
104
+ if mock {
105
+ return " Mock summary from commits with vibe ' \( vibe) ': Enhanced features and fixed bugs based on recent changes. (Commits: \( commits. prefix ( 50 ) ) ...) "
106
+ }
107
+
108
+ guard let scriptURL = Bundle . main. url ( forResource: " summarize " , withExtension: " py " ) else {
109
+ throw VibeCommitError . fileNotFound ( " summarize.py not found in bundle. Check if it's in Sources/VibeCommit/ and Package.swift has .copy( \" summarize.py \" ). Run rm -rf .build && swift build to clean. " )
110
+ }
111
+ let scriptPath = scriptURL. path
112
+ print ( " Debug: Using script at \( scriptPath) " ) // For debugging path
113
+
114
+ let prompt = vibe. isEmpty ? commits : " \( commits) \n Vibe: \( vibe) "
115
+
55
116
let process = Process ( )
56
117
process. executableURL = URL ( fileURLWithPath: " /usr/bin/env " )
57
- process. arguments = [ " python " , scriptPath]
118
+ process. arguments = [ " python3 " , scriptPath] // Use python3 for macOS compatibility
58
119
59
120
let inputPipe = Pipe ( )
60
121
process. standardInput = inputPipe
@@ -65,8 +126,8 @@ struct VibeCommit: ParsableCommand {
65
126
66
127
try process. run ( )
67
128
68
- // Write commits to input
69
- if let data = commits . data ( using: . utf8) {
129
+ // Write prompt to input
130
+ if let data = prompt . data ( using: . utf8) {
70
131
try inputPipe. fileHandleForWriting. write ( contentsOf: data)
71
132
}
72
133
try inputPipe. fileHandleForWriting. close ( )
@@ -76,12 +137,12 @@ struct VibeCommit: ParsableCommand {
76
137
if process. terminationStatus != 0 {
77
138
let errorData = errorPipe. fileHandleForReading. readDataToEndOfFile ( )
78
139
let errorOutput = String ( data: errorData, encoding: . utf8) ?? " Unknown error "
79
- throw NSError ( domain : " AIError " , code : Int ( process. terminationStatus) , userInfo : [ " error " : errorOutput] )
140
+ throw VibeCommitError . aiExecutionFailed ( Int ( process. terminationStatus) , errorOutput)
80
141
}
81
142
82
143
let data = outputPipe. fileHandleForReading. readDataToEndOfFile ( )
83
144
guard let summary = String ( data: data, encoding: . utf8) ? . trimmingCharacters ( in: . whitespacesAndNewlines) else {
84
- throw NSError ( domain : " AIError " , code : 1 , userInfo : nil )
145
+ throw VibeCommitError . invalidOutput
85
146
}
86
147
return summary
87
148
}
@@ -96,10 +157,11 @@ struct VibeCommit: ParsableCommand {
96
157
print ( " Vibe check passed: \( summary) " )
97
158
98
159
if autoCommit {
99
- let confirm = readLine ( prompt: " Auto-commit with this summary? (y/n): " )
100
- if confirm? . lowercased ( ) == " y " {
101
- try shell ( " git add . " )
102
- try shell ( " git commit -m ' \( summary) ' " )
160
+ print ( " Auto-commit with this summary? (y/n): " , terminator: " " )
161
+ let confirm = Swift . readLine ( ) ? . lowercased ( )
162
+ if confirm == " y " {
163
+ _ = try shell ( " git add . " )
164
+ _ = try shell ( " git commit -m ' \( summary) ' " )
103
165
print ( " Committed successfully! " )
104
166
} else {
105
167
print ( " Auto-commit canceled. " )
@@ -128,10 +190,125 @@ struct VibeCommit: ParsableCommand {
128
190
}
129
191
}
130
192
131
- // Extension for readLine with prompt (for confirmation)
132
- extension String {
133
- static func readLine( prompt: String ) -> String ? {
134
- print ( prompt, terminator: " " )
135
- return Swift . readLine ( )
193
+ // SpeechAnalyzer for voice dictation (adapted from Apple docs)
194
+ class SpeechAnalyzer : NSObject , SFSpeechRecognizerDelegate {
195
+ private let audioEngine = AVAudioEngine ( )
196
+ private var inputNode : AVAudioInputNode ?
197
+ private var recognitionRequest : SFSpeechAudioBufferRecognitionRequest ?
198
+ private var recognitionTask : SFSpeechRecognitionTask ?
199
+ private var speechRecognizer = SFSpeechRecognizer ( locale: Locale ( identifier: " en-US " ) ) // Change locale as needed
200
+ private var transcribedText = " "
201
+
202
+ override init ( ) {
203
+ super. init ( )
204
+ speechRecognizer? . delegate = self
205
+ }
206
+
207
+ func dictate( ) throws -> String {
208
+ try requestSpeechPermissions ( )
209
+ try requestMicPermissions ( )
210
+ startRecognition ( )
211
+ print ( " Listening for voice... Press Enter to stop. " )
212
+
213
+ _ = readLine ( )
214
+ stopRecognition ( )
215
+
216
+ return transcribedText
217
+ }
218
+
219
+ private func requestSpeechPermissions( ) throws {
220
+ var permissionGranted = false
221
+ var done = false
222
+
223
+ SFSpeechRecognizer . requestAuthorization { status in
224
+ permissionGranted = status == . authorized
225
+ done = true
226
+ }
227
+
228
+ let timeoutDate = Date ( timeIntervalSinceNow: 10.0 ) // Prevent infinite loop
229
+ while !done && Date ( ) < timeoutDate {
230
+ RunLoop . current. run ( mode: . default, before: Date ( timeIntervalSinceNow: 0.1 ) )
231
+ }
232
+
233
+ if !done {
234
+ throw VibeCommitError . permissionDenied ( " Speech recognition permission request timed out. " )
235
+ }
236
+
237
+ if !permissionGranted {
238
+ throw VibeCommitError . permissionDenied ( " Speech recognition permission denied. " )
239
+ }
240
+ }
241
+
242
+ private func requestMicPermissions( ) throws {
243
+ var permissionGranted = false
244
+ var done = false
245
+
246
+ AVAudioApplication . requestRecordPermission { granted in
247
+ permissionGranted = granted
248
+ done = true
249
+ }
250
+
251
+ let timeoutDate = Date ( timeIntervalSinceNow: 10.0 ) // Prevent infinite loop
252
+ while !done && Date ( ) < timeoutDate {
253
+ RunLoop . current. run ( mode: . default, before: Date ( timeIntervalSinceNow: 0.1 ) )
254
+ }
255
+
256
+ if !done {
257
+ throw VibeCommitError . permissionDenied ( " Microphone permission request timed out. " )
258
+ }
259
+
260
+ if !permissionGranted {
261
+ throw VibeCommitError . permissionDenied ( " Microphone permission denied. " )
262
+ }
263
+ }
264
+
265
+ private func startRecognition( ) {
266
+ inputNode = audioEngine. inputNode
267
+ recognitionRequest = SFSpeechAudioBufferRecognitionRequest ( )
268
+ recognitionRequest? . shouldReportPartialResults = true // For live updates
269
+
270
+ guard let recognitionRequest = recognitionRequest, let inputNode = inputNode else {
271
+ print ( " Unable to start recognition. " )
272
+ return
273
+ }
274
+
275
+ recognitionTask = speechRecognizer? . recognitionTask ( with: recognitionRequest) { result, error in
276
+ if let result = result {
277
+ self . transcribedText = result. bestTranscription. formattedString
278
+ print ( " Live: \( self . transcribedText) " ) // Print live updates
279
+ }
280
+ if error != nil || result? . isFinal == true {
281
+ self . stopRecognition ( )
282
+ }
283
+ }
284
+
285
+ let recordingFormat = inputNode. outputFormat ( forBus: 0 )
286
+ inputNode. installTap ( onBus: 0 , bufferSize: 1024 , format: recordingFormat) { buffer, time in
287
+ recognitionRequest. append ( buffer)
288
+ }
289
+
290
+ audioEngine. prepare ( )
291
+
292
+ do {
293
+ try audioEngine. start ( )
294
+ } catch {
295
+ print ( " Couldn't start audio engine: \( error. localizedDescription) " )
296
+ print ( " If microphone access denied, grant permission in System Settings > Privacy & Security > Microphone for Terminal/Xcode. " )
297
+ stopRecognition ( )
298
+ }
299
+ }
300
+
301
+ private func stopRecognition( ) {
302
+ audioEngine. stop ( )
303
+ inputNode? . removeTap ( onBus: 0 )
304
+ recognitionRequest? . endAudio ( )
305
+ recognitionTask? . cancel ( )
306
+ }
307
+
308
+ func speechRecognizer( _ speechRecognizer: SFSpeechRecognizer , availabilityDidChange available: Bool ) {
309
+ if !available {
310
+ print ( " Speech recognition unavailable. " )
311
+ stopRecognition ( )
312
+ }
136
313
}
137
314
}
0 commit comments