import AVFoundation import Commander import Foundation import Speech import Swabble @MainActor struct TranscribeCommand: ParsableCommand { @Argument(help: "Path to audio/video file") var inputFile: String = "" @Option(name: .long("locale"), help: "Locale identifier", parsing: .singleValue) var locale: String = Locale.current .identifier @Flag(help: "Censor etiquette-sensitive content") var censor: Bool = false @Option(name: .long("output"), help: "Output file path") var outputFile: String? @Option(name: .long("format"), help: "Output format txt|srt") var format: String = "txt" @Option(name: .long("max-length"), help: "Max sentence length for srt") var maxLength: Int = 40 static var commandDescription: CommandDescription { CommandDescription( commandName: "transcribe", abstract: "Transcribe a media file locally") } init() {} init(parsed: ParsedValues) { self.init() if let positional = parsed.positional.first { inputFile = positional } if let loc = parsed.options["locale"]?.last { locale = loc } if parsed.flags.contains("censor") { censor = true } if let out = parsed.options["output"]?.last { outputFile = out } if let fmt = parsed.options["format"]?.last { format = fmt } if let len = parsed.options["maxLength"]?.last, let intVal = Int(len) { maxLength = intVal } } mutating func run() async throws { let fileURL = URL(fileURLWithPath: inputFile) let audioFile = try AVAudioFile(forReading: fileURL) let outputFormat = OutputFormat(rawValue: format) ?? .txt let transcriber = SpeechTranscriber( locale: Locale(identifier: locale), transcriptionOptions: censor ? [.etiquetteReplacements] : [], reportingOptions: [], attributeOptions: outputFormat.needsAudioTimeRange ? [.audioTimeRange] : []) let analyzer = SpeechAnalyzer(modules: [transcriber]) try await analyzer.start(inputAudioFile: audioFile, finishAfterFile: true) var transcript: AttributedString = "" for try await result in transcriber.results { transcript += result.text } let output = outputFormat.text(for: transcript, maxLength: maxLength) if let path = outputFile { try output.write(to: URL(fileURLWithPath: path), atomically: false, encoding: .utf8) } else { print(output) } } }