//
// AgentCommand+Audio.swift
// PeekabooCLI
//
import Darwin
import Dispatch
import Foundation
import PeekabooCore
@available(macOS 14.0, *)
extension AgentCommand {
func buildExecutionTask() async throws -> String? {
if self.audio || self.audioFile != nil {
return try await self.processAudioInput()
}
guard let providedTask = self.task else {
self.printMissingTaskError(message: "Task argument is required", usage: "")
return nil
}
return providedTask
}
private func processAudioInput() async throws -> String? {
self.logAudioStartMessage()
let audioService = self.services.audioInput
do {
let transcript = try await self.transcribeAudio(using: audioService)
self.logTranscriptionSuccess(transcript)
return self.composeExecutionTask(with: transcript)
} catch {
self.logAudioError(error)
return nil
}
}
private func logAudioStartMessage() {
guard !self.jsonOutput && !self.quiet else { return }
if let audioPath = self.audioFile {
print("\(TerminalColor.cyan)🎙️ Processing audio file: \(audioPath)\(TerminalColor.reset)")
} else {
let recordingMessage = [
"\(TerminalColor.cyan)🎙️ Starting audio recording...",
" (Press Ctrl+C to stop)\(TerminalColor.reset)"
].joined()
print(recordingMessage)
}
}
private func transcribeAudio(using audioService: AudioInputService) async throws -> String {
if let audioPath = self.audioFile {
let url = URL(fileURLWithPath: audioPath)
return try await audioService.transcribeAudioFile(url)
} else {
try await audioService.startRecording()
return try await self.captureMicrophoneAudio(using: audioService)
}
}
private func captureMicrophoneAudio(using audioService: AudioInputService) async throws -> String {
try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
let signalSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
signalSource.setEventHandler {
signalSource.cancel()
Task { @MainActor in
do {
let transcript = try await audioService.stopRecording()
continuation.resume(returning: transcript)
} catch {
continuation.resume(throwing: error)
}
}
}
signalSource.resume()
}
} onCancel: {
Task { @MainActor in
_ = try? await audioService.stopRecording()
}
}
}
private func logTranscriptionSuccess(_ transcript: String) {
guard !self.jsonOutput && !self.quiet else { return }
let message = [
"\(TerminalColor.green)\(AgentDisplayTokens.Status.success) Transcription complete",
"\(TerminalColor.reset)"
].joined()
print(message)
print("\(TerminalColor.gray)Transcript: \(transcript.prefix(100))...\(TerminalColor.reset)")
}
private func composeExecutionTask(with transcript: String) -> String {
Self.composeExecutionTask(providedTask: self.task, transcript: transcript)
}
static func composeExecutionTask(providedTask: String?, transcript: String) -> String {
guard let providedTask, !providedTask.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return transcript
}
return "\(providedTask)\n\nAudio transcript:\n\(transcript)"
}
private func logAudioError(_ error: any Error) {
let message = AgentMessages.Audio.processingError(error)
if self.jsonOutput {
let errorObj = [
"success": false,
"error": message
] as [String: Any]
if let jsonData = try? JSONSerialization.data(withJSONObject: errorObj, options: .prettyPrinted),
let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
} else {
print("{\"success\":false,\"error\":\"\(AgentMessages.Audio.genericProcessingError)\"}")
}
} else {
let failurePrefix = [
"\(TerminalColor.red)\(AgentDisplayTokens.Status.failure)",
" ",
message
].joined()
let audioErrorMessage = [failurePrefix, "\(TerminalColor.reset)"].joined()
print(audioErrorMessage)
}
}
}