Skip to content

Commit c3bc61a

Browse files
committed
bug fixes
1 parent 3e56c99 commit c3bc61a

File tree

4 files changed

+225
-23
lines changed

4 files changed

+225
-23
lines changed

Package.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,31 @@ import PackageDescription
33

44
let package = Package(
55
name: "VibeCommit",
6-
platforms: [.macOS(.v14)], // macOS 14+ for stability
6+
platforms: [.macOS(.v14)],
77
products: [
88
.executable(name: "VibeCommit", targets: ["VibeCommit"]),
99
],
1010
dependencies: [
11-
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), // Compatible with 6.2
11+
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
1212
],
1313
targets: [
1414
.executableTarget(
1515
name: "VibeCommit",
1616
dependencies: [
1717
.product(name: "ArgumentParser", package: "swift-argument-parser"),
18+
],
19+
resources: [
20+
.copy("summarize.py")
21+
],
22+
linkerSettings: [
23+
.linkedFramework("AVFoundation"),
24+
.linkedFramework("Speech"),
25+
.unsafeFlags([
26+
"-Xlinker", "-sectcreate",
27+
"-Xlinker", "__TEXT",
28+
"-Xlinker", "__info_plist",
29+
"-Xlinker", "Sources/VibeCommit/Info.plist"
30+
])
1831
]
1932
),
2033
]

Sources/VibeCommit/Info.plist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSMicrophoneUsageDescription</key>
6+
<string>VibeCommit needs microphone access for voice dictation to input vibes.</string>
7+
<key>NSSpeechRecognitionUsageDescription</key>
8+
<string>VibeCommit needs speech recognition access to transcribe your voice input for vibes.</string>
9+
</dict>
10+
</plist>

Sources/VibeCommit/VibeCommit.swift

Lines changed: 198 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
import Foundation
22
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+
}
326

427
@main
528
struct VibeCommit: ParsableCommand {
@@ -13,17 +36,44 @@ struct VibeCommit: ParsableCommand {
1336
@Flag(name: .shortAndLong, help: "Automatically commit using the generated summary.")
1437
var autoCommit: Bool = false
1538

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+
1645
mutating func run() throws {
1746
guard isGitRepo() else {
1847
print("Error: Not a Git repository.")
1948
return
2049
}
2150

2251
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+
}
2777
} else {
2878
print("Run with --summarize to get started.")
2979
}
@@ -49,12 +99,23 @@ struct VibeCommit: ParsableCommand {
4999
return output
50100
}
51101

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)\nVibe: \(vibe)"
115+
55116
let process = Process()
56117
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
57-
process.arguments = ["python", scriptPath]
118+
process.arguments = ["python3", scriptPath] // Use python3 for macOS compatibility
58119

59120
let inputPipe = Pipe()
60121
process.standardInput = inputPipe
@@ -65,8 +126,8 @@ struct VibeCommit: ParsableCommand {
65126

66127
try process.run()
67128

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) {
70131
try inputPipe.fileHandleForWriting.write(contentsOf: data)
71132
}
72133
try inputPipe.fileHandleForWriting.close()
@@ -76,12 +137,12 @@ struct VibeCommit: ParsableCommand {
76137
if process.terminationStatus != 0 {
77138
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
78139
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)
80141
}
81142

82143
let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
83144
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
85146
}
86147
return summary
87148
}
@@ -96,10 +157,11 @@ struct VibeCommit: ParsableCommand {
96157
print("Vibe check passed: \(summary)")
97158

98159
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)'")
103165
print("Committed successfully!")
104166
} else {
105167
print("Auto-commit canceled.")
@@ -128,10 +190,125 @@ struct VibeCommit: ParsableCommand {
128190
}
129191
}
130192

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+
}
136313
}
137314
}

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
transformers
2+
torch

0 commit comments

Comments
 (0)