chore(swabble): apply swiftformat
parent
336c9d6caa
commit
318457cb2c
|
|
@ -33,5 +33,4 @@ let package = Package(
|
||||||
.product(name: "Testing", package: "swift-testing"),
|
.product(name: "Testing", package: "swift-testing"),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
swiftLanguageModes: [.v6]
|
swiftLanguageModes: [.v6])
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@ public actor HookRunner {
|
||||||
|
|
||||||
public init(config: SwabbleConfig) {
|
public init(config: SwabbleConfig) {
|
||||||
self.config = config
|
self.config = config
|
||||||
self.hostname = Host.current().localizedName ?? "host"
|
hostname = Host.current().localizedName ?? "host"
|
||||||
}
|
}
|
||||||
|
|
||||||
public func shouldRun() -> Bool {
|
public func shouldRun() -> Bool {
|
||||||
guard self.config.hook.cooldownSeconds > 0 else { return true }
|
guard config.hook.cooldownSeconds > 0 else { return true }
|
||||||
if let lastRun, Date().timeIntervalSince(lastRun) < config.hook.cooldownSeconds {
|
if let lastRun, Date().timeIntervalSince(lastRun) < config.hook.cooldownSeconds {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -29,23 +29,23 @@ public actor HookRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func run(job: HookJob) async throws {
|
public func run(job: HookJob) async throws {
|
||||||
guard self.shouldRun() else { return }
|
guard shouldRun() else { return }
|
||||||
guard !self.config.hook.command.isEmpty else { throw NSError(
|
guard !config.hook.command.isEmpty else { throw NSError(
|
||||||
domain: "Hook",
|
domain: "Hook",
|
||||||
code: 1,
|
code: 1,
|
||||||
userInfo: [NSLocalizedDescriptionKey: "hook command not set"]) }
|
userInfo: [NSLocalizedDescriptionKey: "hook command not set"]) }
|
||||||
|
|
||||||
let prefix = self.config.hook.prefix.replacingOccurrences(of: "${hostname}", with: self.hostname)
|
let prefix = config.hook.prefix.replacingOccurrences(of: "${hostname}", with: hostname)
|
||||||
let payload = prefix + job.text
|
let payload = prefix + job.text
|
||||||
|
|
||||||
let process = Process()
|
let process = Process()
|
||||||
process.executableURL = URL(fileURLWithPath: self.config.hook.command)
|
process.executableURL = URL(fileURLWithPath: config.hook.command)
|
||||||
process.arguments = self.config.hook.args + [payload]
|
process.arguments = config.hook.args + [payload]
|
||||||
|
|
||||||
var env = ProcessInfo.processInfo.environment
|
var env = ProcessInfo.processInfo.environment
|
||||||
env["SWABBLE_TEXT"] = job.text
|
env["SWABBLE_TEXT"] = job.text
|
||||||
env["SWABBLE_PREFIX"] = prefix
|
env["SWABBLE_PREFIX"] = prefix
|
||||||
for (k, v) in self.config.hook.env {
|
for (k, v) in config.hook.env {
|
||||||
env[k] = v
|
env[k] = v
|
||||||
}
|
}
|
||||||
process.environment = env
|
process.environment = env
|
||||||
|
|
@ -70,6 +70,6 @@ public actor HookRunner {
|
||||||
try await group.next()
|
try await group.next()
|
||||||
group.cancelAll()
|
group.cancelAll()
|
||||||
}
|
}
|
||||||
self.lastRun = Date()
|
lastRun = Date()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,18 +35,18 @@ public actor SpeechPipeline {
|
||||||
transcriptionOptions: etiquette ? [.etiquetteReplacements] : [],
|
transcriptionOptions: etiquette ? [.etiquetteReplacements] : [],
|
||||||
reportingOptions: [.volatileResults],
|
reportingOptions: [.volatileResults],
|
||||||
attributeOptions: [])
|
attributeOptions: [])
|
||||||
self.transcriber = transcriberModule
|
transcriber = transcriberModule
|
||||||
|
|
||||||
guard let analyzerFormat = await SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith: [transcriberModule])
|
guard let analyzerFormat = await SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith: [transcriberModule])
|
||||||
else {
|
else {
|
||||||
throw SpeechPipelineError.analyzerFormatUnavailable
|
throw SpeechPipelineError.analyzerFormatUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
self.analyzer = SpeechAnalyzer(modules: [transcriberModule])
|
analyzer = SpeechAnalyzer(modules: [transcriberModule])
|
||||||
let (stream, continuation) = AsyncStream<AnalyzerInput>.makeStream()
|
let (stream, continuation) = AsyncStream<AnalyzerInput>.makeStream()
|
||||||
self.inputContinuation = continuation
|
inputContinuation = continuation
|
||||||
|
|
||||||
let inputNode = self.engine.inputNode
|
let inputNode = engine.inputNode
|
||||||
let inputFormat = inputNode.outputFormat(forBus: 0)
|
let inputFormat = inputNode.outputFormat(forBus: 0)
|
||||||
inputNode.removeTap(onBus: 0)
|
inputNode.removeTap(onBus: 0)
|
||||||
inputNode.installTap(onBus: 0, bufferSize: 2048, format: inputFormat) { [weak self] buffer, _ in
|
inputNode.installTap(onBus: 0, bufferSize: 2048, format: inputFormat) { [weak self] buffer, _ in
|
||||||
|
|
@ -55,11 +55,11 @@ public actor SpeechPipeline {
|
||||||
Task { await self.handleBuffer(boxed.buffer, targetFormat: analyzerFormat) }
|
Task { await self.handleBuffer(boxed.buffer, targetFormat: analyzerFormat) }
|
||||||
}
|
}
|
||||||
|
|
||||||
self.engine.prepare()
|
engine.prepare()
|
||||||
try self.engine.start()
|
try engine.start()
|
||||||
try await self.analyzer?.start(inputSequence: stream)
|
try await analyzer?.start(inputSequence: stream)
|
||||||
|
|
||||||
guard let transcriberForStream = self.transcriber else {
|
guard let transcriberForStream = transcriber else {
|
||||||
throw SpeechPipelineError.transcriberUnavailable
|
throw SpeechPipelineError.transcriberUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,18 +82,18 @@ public actor SpeechPipeline {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func stop() async {
|
public func stop() async {
|
||||||
self.resultTask?.cancel()
|
resultTask?.cancel()
|
||||||
self.inputContinuation?.finish()
|
inputContinuation?.finish()
|
||||||
self.engine.inputNode.removeTap(onBus: 0)
|
engine.inputNode.removeTap(onBus: 0)
|
||||||
self.engine.stop()
|
engine.stop()
|
||||||
try? await self.analyzer?.finalizeAndFinishThroughEndOfInput()
|
try? await analyzer?.finalizeAndFinishThroughEndOfInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleBuffer(_ buffer: AVAudioPCMBuffer, targetFormat: AVAudioFormat) async {
|
private func handleBuffer(_ buffer: AVAudioPCMBuffer, targetFormat: AVAudioFormat) async {
|
||||||
do {
|
do {
|
||||||
let converted = try converter.convert(buffer, to: targetFormat)
|
let converted = try converter.convert(buffer, to: targetFormat)
|
||||||
let input = AnalyzerInput(buffer: converted)
|
let input = AnalyzerInput(buffer: converted)
|
||||||
self.inputContinuation?.yield(input)
|
inputContinuation?.yield(input)
|
||||||
} catch {
|
} catch {
|
||||||
// drop on conversion failure
|
// drop on conversion failure
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,11 @@ public struct Logger: Sendable {
|
||||||
print("[\(level.rawValue.uppercased())] \(ts) | \(message)")
|
print("[\(level.rawValue.uppercased())] \(ts) | \(message)")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func trace(_ msg: String) { self.log(.trace, msg) }
|
public func trace(_ msg: String) { log(.trace, msg) }
|
||||||
public func debug(_ msg: String) { self.log(.debug, msg) }
|
public func debug(_ msg: String) { log(.debug, msg) }
|
||||||
public func info(_ msg: String) { self.log(.info, msg) }
|
public func info(_ msg: String) { log(.info, msg) }
|
||||||
public func warn(_ msg: String) { self.log(.warn, msg) }
|
public func warn(_ msg: String) { log(.warn, msg) }
|
||||||
public func error(_ msg: String) { self.log(.error, msg) }
|
public func error(_ msg: String) { log(.error, msg) }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LogLevel {
|
extension LogLevel {
|
||||||
|
|
|
||||||
|
|
@ -11,24 +11,24 @@ public actor TranscriptsStore {
|
||||||
let dir = FileManager.default.homeDirectoryForCurrentUser
|
let dir = FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent("Library/Application Support/swabble", isDirectory: true)
|
.appendingPathComponent("Library/Application Support/swabble", isDirectory: true)
|
||||||
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||||
self.fileURL = dir.appendingPathComponent("transcripts.log")
|
fileURL = dir.appendingPathComponent("transcripts.log")
|
||||||
if let data = try? Data(contentsOf: fileURL),
|
if let data = try? Data(contentsOf: fileURL),
|
||||||
let text = String(data: data, encoding: .utf8)
|
let text = String(data: data, encoding: .utf8)
|
||||||
{
|
{
|
||||||
self.entries = text.split(separator: "\n").map(String.init).suffix(self.limit)
|
entries = text.split(separator: "\n").map(String.init).suffix(limit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func append(text: String) {
|
public func append(text: String) {
|
||||||
self.entries.append(text)
|
entries.append(text)
|
||||||
if self.entries.count > self.limit {
|
if entries.count > limit {
|
||||||
self.entries.removeFirst(self.entries.count - self.limit)
|
entries.removeFirst(entries.count - limit)
|
||||||
}
|
}
|
||||||
let body = self.entries.joined(separator: "\n")
|
let body = entries.joined(separator: "\n")
|
||||||
try? body.write(to: self.fileURL, atomically: false, encoding: .utf8)
|
try? body.write(to: fileURL, atomically: false, encoding: .utf8)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func latest() -> [String] { self.entries }
|
public func latest() -> [String] { entries }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ struct DoctorCommand: ParsableCommand {
|
||||||
init() {}
|
init() {}
|
||||||
init(parsed: ParsedValues) {
|
init(parsed: ParsedValues) {
|
||||||
self.init()
|
self.init()
|
||||||
if let cfg = parsed.options["config"]?.last { self.configPath = cfg }
|
if let cfg = parsed.options["config"]?.last { configPath = cfg }
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
let auth = await SFSpeechRecognizer.authorizationStatus()
|
let auth = await SFSpeechRecognizer.authorizationStatus()
|
||||||
print("Speech auth: \(auth)")
|
print("Speech auth: \(auth)")
|
||||||
do {
|
do {
|
||||||
_ = try ConfigLoader.load(at: self.configURL)
|
_ = try ConfigLoader.load(at: configURL)
|
||||||
print("Config: OK")
|
print("Config: OK")
|
||||||
} catch {
|
} catch {
|
||||||
print("Config missing or invalid; run setup")
|
print("Config missing or invalid; run setup")
|
||||||
|
|
@ -33,5 +33,5 @@ struct DoctorCommand: ParsableCommand {
|
||||||
print("Mics found: \(session.devices.count)")
|
print("Mics found: \(session.devices.count)")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var configURL: URL? { self.configPath.map { URL(fileURLWithPath: $0) } }
|
private var configURL: URL? { configPath.map { URL(fileURLWithPath: $0) } }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,16 @@ struct MicSet: ParsableCommand {
|
||||||
init() {}
|
init() {}
|
||||||
init(parsed: ParsedValues) {
|
init(parsed: ParsedValues) {
|
||||||
self.init()
|
self.init()
|
||||||
if let value = parsed.positional.first, let intVal = Int(value) { self.index = intVal }
|
if let value = parsed.positional.first, let intVal = Int(value) { index = intVal }
|
||||||
if let cfg = parsed.options["config"]?.last { self.configPath = cfg }
|
if let cfg = parsed.options["config"]?.last { configPath = cfg }
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
var cfg = try ConfigLoader.load(at: self.configURL)
|
var cfg = try ConfigLoader.load(at: configURL)
|
||||||
cfg.audio.deviceIndex = self.index
|
cfg.audio.deviceIndex = index
|
||||||
try ConfigLoader.save(cfg, at: self.configURL)
|
try ConfigLoader.save(cfg, at: configURL)
|
||||||
print("saved device index \(self.index)")
|
print("saved device index \(index)")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var configURL: URL? { self.configPath.map { URL(fileURLWithPath: $0) } }
|
private var configURL: URL? { configPath.map { URL(fileURLWithPath: $0) } }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,19 +17,19 @@ struct ServeCommand: ParsableCommand {
|
||||||
|
|
||||||
init(parsed: ParsedValues) {
|
init(parsed: ParsedValues) {
|
||||||
self.init()
|
self.init()
|
||||||
if parsed.flags.contains("noWake") { self.noWake = true }
|
if parsed.flags.contains("noWake") { noWake = true }
|
||||||
if let cfg = parsed.options["config"]?.last { self.configPath = cfg }
|
if let cfg = parsed.options["config"]?.last { configPath = cfg }
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
var cfg: SwabbleConfig
|
var cfg: SwabbleConfig
|
||||||
do {
|
do {
|
||||||
cfg = try ConfigLoader.load(at: self.configURL)
|
cfg = try ConfigLoader.load(at: configURL)
|
||||||
} catch {
|
} catch {
|
||||||
cfg = SwabbleConfig()
|
cfg = SwabbleConfig()
|
||||||
try ConfigLoader.save(cfg, at: self.configURL)
|
try ConfigLoader.save(cfg, at: configURL)
|
||||||
}
|
}
|
||||||
if self.noWake {
|
if noWake {
|
||||||
cfg.wake.enabled = false
|
cfg.wake.enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +64,7 @@ struct ServeCommand: ParsableCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var configURL: URL? {
|
private var configURL: URL? {
|
||||||
self.configPath.map { URL(fileURLWithPath: $0) }
|
configPath.map { URL(fileURLWithPath: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func matchesWake(text: String, cfg: SwabbleConfig) -> Bool {
|
private static func matchesWake(text: String, cfg: SwabbleConfig) -> Bool {
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,11 @@ private enum LaunchdHelper {
|
||||||
"KeepAlive": true,
|
"KeepAlive": true,
|
||||||
]
|
]
|
||||||
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
|
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
|
||||||
try data.write(to: self.plistURL)
|
try data.write(to: plistURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func removePlist() throws {
|
static func removePlist() throws {
|
||||||
try? FileManager.default.removeItem(at: self.plistURL)
|
try? FileManager.default.removeItem(at: plistURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,14 @@ struct SetupCommand: ParsableCommand {
|
||||||
init() {}
|
init() {}
|
||||||
init(parsed: ParsedValues) {
|
init(parsed: ParsedValues) {
|
||||||
self.init()
|
self.init()
|
||||||
if let cfg = parsed.options["config"]?.last { self.configPath = cfg }
|
if let cfg = parsed.options["config"]?.last { configPath = cfg }
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
let cfg = SwabbleConfig()
|
let cfg = SwabbleConfig()
|
||||||
try ConfigLoader.save(cfg, at: self.configURL)
|
try ConfigLoader.save(cfg, at: configURL)
|
||||||
print("wrote config to \(self.configURL?.path ?? SwabbleConfig.defaultPath.path)")
|
print("wrote config to \(configURL?.path ?? SwabbleConfig.defaultPath.path)")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var configURL: URL? { self.configPath.map { URL(fileURLWithPath: $0) } }
|
private var configURL: URL? { configPath.map { URL(fileURLWithPath: $0) } }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@ struct StatusCommand: ParsableCommand {
|
||||||
init() {}
|
init() {}
|
||||||
init(parsed: ParsedValues) {
|
init(parsed: ParsedValues) {
|
||||||
self.init()
|
self.init()
|
||||||
if let cfg = parsed.options["config"]?.last { self.configPath = cfg }
|
if let cfg = parsed.options["config"]?.last { configPath = cfg }
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
let cfg = try? ConfigLoader.load(at: self.configURL)
|
let cfg = try? ConfigLoader.load(at: configURL)
|
||||||
let wake = cfg?.wake.word ?? "clawd"
|
let wake = cfg?.wake.word ?? "clawd"
|
||||||
let wakeEnabled = cfg?.wake.enabled ?? false
|
let wakeEnabled = cfg?.wake.enabled ?? false
|
||||||
let latest = await TranscriptsStore.shared.latest().suffix(3)
|
let latest = await TranscriptsStore.shared.latest().suffix(3)
|
||||||
|
|
@ -30,5 +30,5 @@ struct StatusCommand: ParsableCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var configURL: URL? { self.configPath.map { URL(fileURLWithPath: $0) } }
|
private var configURL: URL? { configPath.map { URL(fileURLWithPath: $0) } }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,16 @@ struct TestHookCommand: ParsableCommand {
|
||||||
|
|
||||||
init(parsed: ParsedValues) {
|
init(parsed: ParsedValues) {
|
||||||
self.init()
|
self.init()
|
||||||
if let positional = parsed.positional.first { self.text = positional }
|
if let positional = parsed.positional.first { text = positional }
|
||||||
if let cfg = parsed.options["config"]?.last { self.configPath = cfg }
|
if let cfg = parsed.options["config"]?.last { configPath = cfg }
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
let cfg = try ConfigLoader.load(at: self.configURL)
|
let cfg = try ConfigLoader.load(at: configURL)
|
||||||
let runner = HookRunner(config: cfg)
|
let runner = HookRunner(config: cfg)
|
||||||
try await runner.run(job: HookJob(text: self.text, timestamp: Date()))
|
try await runner.run(job: HookJob(text: text, timestamp: Date()))
|
||||||
print("hook invoked")
|
print("hook invoked")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var configURL: URL? { self.configPath.map { URL(fileURLWithPath: $0) } }
|
private var configURL: URL? { configPath.map { URL(fileURLWithPath: $0) } }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,12 @@ struct TranscribeCommand: ParsableCommand {
|
||||||
|
|
||||||
init(parsed: ParsedValues) {
|
init(parsed: ParsedValues) {
|
||||||
self.init()
|
self.init()
|
||||||
if let positional = parsed.positional.first { self.inputFile = positional }
|
if let positional = parsed.positional.first { inputFile = positional }
|
||||||
if let loc = parsed.options["locale"]?.last { self.locale = loc }
|
if let loc = parsed.options["locale"]?.last { locale = loc }
|
||||||
if parsed.flags.contains("censor") { self.censor = true }
|
if parsed.flags.contains("censor") { censor = true }
|
||||||
if let out = parsed.options["output"]?.last { self.outputFile = out }
|
if let out = parsed.options["output"]?.last { outputFile = out }
|
||||||
if let fmt = parsed.options["format"]?.last { self.format = fmt }
|
if let fmt = parsed.options["format"]?.last { format = fmt }
|
||||||
if let len = parsed.options["maxLength"]?.last, let intVal = Int(len) { self.maxLength = intVal }
|
if let len = parsed.options["maxLength"]?.last, let intVal = Int(len) { maxLength = intVal }
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
|
|
@ -51,7 +51,7 @@ struct TranscribeCommand: ParsableCommand {
|
||||||
transcript += result.text
|
transcript += result.text
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = outputFormat.text(for: transcript, maxLength: self.maxLength)
|
let output = outputFormat.text(for: transcript, maxLength: maxLength)
|
||||||
if let path = outputFile {
|
if let path = outputFile {
|
||||||
try output.write(to: URL(fileURLWithPath: path), atomically: false, encoding: .utf8)
|
try output.write(to: URL(fileURLWithPath: path), atomically: false, encoding: .utf8)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue