85 lines
3.0 KiB
Swift
85 lines
3.0 KiB
Swift
import Commander
|
|
import Foundation
|
|
import Swabble
|
|
|
|
@MainActor
|
|
struct ServeCommand: ParsableCommand {
|
|
@Option(name: .long("config"), help: "Path to config JSON") var configPath: String?
|
|
@Flag(name: .long("no-wake"), help: "Disable wake word") var noWake: Bool = false
|
|
|
|
static var commandDescription: CommandDescription {
|
|
CommandDescription(
|
|
commandName: "serve",
|
|
abstract: "Run swabble in the foreground")
|
|
}
|
|
|
|
init() {}
|
|
|
|
init(parsed: ParsedValues) {
|
|
self.init()
|
|
if parsed.flags.contains("noWake") { self.noWake = true }
|
|
if let cfg = parsed.options["config"]?.last { self.configPath = cfg }
|
|
}
|
|
|
|
mutating func run() async throws {
|
|
var cfg: SwabbleConfig
|
|
do {
|
|
cfg = try ConfigLoader.load(at: self.configURL)
|
|
} catch {
|
|
cfg = SwabbleConfig()
|
|
try ConfigLoader.save(cfg, at: self.configURL)
|
|
}
|
|
if self.noWake {
|
|
cfg.wake.enabled = false
|
|
}
|
|
|
|
let logger = Logger(level: LogLevel(configValue: cfg.logging.level) ?? .info)
|
|
logger.info("swabble serve starting (wake: \(cfg.wake.enabled ? cfg.wake.word : "disabled"))")
|
|
let pipeline = SpeechPipeline()
|
|
do {
|
|
let stream = try await pipeline.start(
|
|
localeIdentifier: cfg.speech.localeIdentifier,
|
|
etiquette: cfg.speech.etiquetteReplacements)
|
|
for await seg in stream {
|
|
if cfg.wake.enabled {
|
|
guard Self.matchesWake(text: seg.text, cfg: cfg) else { continue }
|
|
}
|
|
let stripped = Self.stripWake(text: seg.text, cfg: cfg)
|
|
let job = HookJob(text: stripped, timestamp: Date())
|
|
let runner = HookRunner(config: cfg)
|
|
try await runner.run(job: job)
|
|
if cfg.transcripts.enabled {
|
|
await TranscriptsStore.shared.append(text: stripped)
|
|
}
|
|
if seg.isFinal {
|
|
logger.info("final: \(stripped)")
|
|
} else {
|
|
logger.debug("partial: \(stripped)")
|
|
}
|
|
}
|
|
} catch {
|
|
logger.error("serve error: \(error)")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
private var configURL: URL? {
|
|
self.configPath.map { URL(fileURLWithPath: $0) }
|
|
}
|
|
|
|
private static func matchesWake(text: String, cfg: SwabbleConfig) -> Bool {
|
|
let lowered = text.lowercased()
|
|
if lowered.contains(cfg.wake.word.lowercased()) { return true }
|
|
return cfg.wake.aliases.contains(where: { lowered.contains($0.lowercased()) })
|
|
}
|
|
|
|
private static func stripWake(text: String, cfg: SwabbleConfig) -> String {
|
|
var out = text
|
|
out = out.replacingOccurrences(of: cfg.wake.word, with: "", options: [.caseInsensitive])
|
|
for alias in cfg.wake.aliases {
|
|
out = out.replacingOccurrences(of: alias, with: "", options: [.caseInsensitive])
|
|
}
|
|
return out.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
}
|