feat: share wake gate via SwabbleKit
This commit is contained in:
11
Swabble/CHANGELOG.md
Normal file
11
Swabble/CHANGELOG.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.0 — 2025-12-23
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
- Added `SwabbleKit` (multi-platform wake-word gate utilities with segment-aware gap detection).
|
||||||
|
- Swabble package now supports iOS + macOS consumers; CLI remains macOS 26-only.
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- CLI wake-word matching/stripping routed through `SwabbleKit` helpers.
|
||||||
|
- Speech pipeline types now explicitly gated to macOS 26 / iOS 26 availability.
|
||||||
@@ -4,10 +4,12 @@ import PackageDescription
|
|||||||
let package = Package(
|
let package = Package(
|
||||||
name: "swabble",
|
name: "swabble",
|
||||||
platforms: [
|
platforms: [
|
||||||
.macOS(.v26),
|
.macOS(.v15),
|
||||||
|
.iOS(.v17),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(name: "Swabble", targets: ["Swabble"]),
|
.library(name: "Swabble", targets: ["Swabble"]),
|
||||||
|
.library(name: "SwabbleKit", targets: ["SwabbleKit"]),
|
||||||
.executable(name: "swabble", targets: ["SwabbleCLI"]),
|
.executable(name: "swabble", targets: ["SwabbleCLI"]),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
@@ -19,13 +21,30 @@ let package = Package(
|
|||||||
name: "Swabble",
|
name: "Swabble",
|
||||||
path: "Sources/SwabbleCore",
|
path: "Sources/SwabbleCore",
|
||||||
swiftSettings: []),
|
swiftSettings: []),
|
||||||
|
.target(
|
||||||
|
name: "SwabbleKit",
|
||||||
|
path: "Sources/SwabbleKit",
|
||||||
|
swiftSettings: [
|
||||||
|
.enableUpcomingFeature("StrictConcurrency"),
|
||||||
|
]),
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "SwabbleCLI",
|
name: "SwabbleCLI",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"Swabble",
|
"Swabble",
|
||||||
|
"SwabbleKit",
|
||||||
.product(name: "Commander", package: "Commander"),
|
.product(name: "Commander", package: "Commander"),
|
||||||
],
|
],
|
||||||
path: "Sources/swabble"),
|
path: "Sources/swabble"),
|
||||||
|
.testTarget(
|
||||||
|
name: "SwabbleKitTests",
|
||||||
|
dependencies: [
|
||||||
|
"SwabbleKit",
|
||||||
|
.product(name: "Testing", package: "swift-testing"),
|
||||||
|
],
|
||||||
|
swiftSettings: [
|
||||||
|
.enableUpcomingFeature("StrictConcurrency"),
|
||||||
|
.enableExperimentalFeature("SwiftTesting"),
|
||||||
|
]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "swabbleTests",
|
name: "swabbleTests",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# 🎙️ swabble — Speech.framework wake-word hook daemon (macOS 26)
|
# 🎙️ swabble — Speech.framework wake-word hook daemon (macOS 26)
|
||||||
|
|
||||||
swabble is a Swift 6.2, macOS 26-only rewrite of the brabble voice daemon. It listens on your mic, gates on a wake word, transcribes locally using Apple's new SpeechAnalyzer + SpeechTranscriber, then fires a shell hook with the transcript. No cloud calls, no Whisper binaries.
|
swabble is a Swift 6.2 wake-word hook daemon. The CLI targets macOS 26 (SpeechAnalyzer + SpeechTranscriber). The shared `SwabbleKit` target is multi-platform and exposes wake-word gating utilities for iOS/macOS apps.
|
||||||
|
|
||||||
- **Local-only**: Speech.framework on-device models; zero network usage.
|
- **Local-only**: Speech.framework on-device models; zero network usage.
|
||||||
- **Wake word**: Default `clawd` (aliases `claude`), optional `--no-wake` bypass.
|
- **Wake word**: Default `clawd` (aliases `claude`), optional `--no-wake` bypass.
|
||||||
|
- **SwabbleKit**: Shared wake gate utilities (gap-based gating when you provide speech segments).
|
||||||
- **Hooks**: Run any command with prefix/env, cooldown, min_chars, timeout.
|
- **Hooks**: Run any command with prefix/env, cooldown, min_chars, timeout.
|
||||||
- **Services**: launchd helper stubs for start/stop/install.
|
- **Services**: launchd helper stubs for start/stop/install.
|
||||||
- **File transcribe**: TXT or SRT with time ranges (using AttributedString splits).
|
- **File transcribe**: TXT or SRT with time ranges (using AttributedString splits).
|
||||||
@@ -30,7 +31,7 @@ swift run swabble transcribe /path/to/audio.m4a --format srt --output out.srt
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Use as a library
|
## Use as a library
|
||||||
Add swabble as a SwiftPM dependency and import the `Swabble` product to reuse the Speech pipeline, config loader, hook executor, and transcript store in your own app:
|
Add swabble as a SwiftPM dependency and import the `Swabble` or `SwabbleKit` product:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Package.swift
|
// Package.swift
|
||||||
@@ -38,7 +39,10 @@ dependencies: [
|
|||||||
.package(url: "https://github.com/steipete/swabble.git", branch: "main"),
|
.package(url: "https://github.com/steipete/swabble.git", branch: "main"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(name: "MyApp", dependencies: [.product(name: "Swabble", package: "swabble")]),
|
.target(name: "MyApp", dependencies: [
|
||||||
|
.product(name: "Swabble", package: "swabble"), // Speech pipeline (macOS 26+ / iOS 26+)
|
||||||
|
.product(name: "SwabbleKit", package: "swabble"), // Wake-word gate utilities (iOS 17+ / macOS 15+)
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -93,7 +97,7 @@ Environment variables:
|
|||||||
|
|
||||||
## Speech pipeline
|
## Speech pipeline
|
||||||
- `AVAudioEngine` tap → `BufferConverter` → `AnalyzerInput` → `SpeechAnalyzer` with a `SpeechTranscriber` module.
|
- `AVAudioEngine` tap → `BufferConverter` → `AnalyzerInput` → `SpeechAnalyzer` with a `SpeechTranscriber` module.
|
||||||
- Requests volatile + final results; wake gating is string match on partial/final.
|
- Requests volatile + final results; the CLI uses text-only wake gating today.
|
||||||
- Authorization requested at first start; requires macOS 26 + new Speech.framework APIs.
|
- Authorization requested at first start; requires macOS 26 + new Speech.framework APIs.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import AVFoundation
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Speech
|
import Speech
|
||||||
|
|
||||||
|
@available(macOS 26.0, iOS 26.0, *)
|
||||||
public struct SpeechSegment: Sendable {
|
public struct SpeechSegment: Sendable {
|
||||||
public let text: String
|
public let text: String
|
||||||
public let isFinal: Bool
|
public let isFinal: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(macOS 26.0, iOS 26.0, *)
|
||||||
public enum SpeechPipelineError: Error {
|
public enum SpeechPipelineError: Error {
|
||||||
case authorizationDenied
|
case authorizationDenied
|
||||||
case analyzerFormatUnavailable
|
case analyzerFormatUnavailable
|
||||||
@@ -14,6 +16,7 @@ public enum SpeechPipelineError: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Live microphone → SpeechAnalyzer → SpeechTranscriber pipeline.
|
/// Live microphone → SpeechAnalyzer → SpeechTranscriber pipeline.
|
||||||
|
@available(macOS 26.0, iOS 26.0, *)
|
||||||
public actor SpeechPipeline {
|
public actor SpeechPipeline {
|
||||||
private struct UnsafeBuffer: @unchecked Sendable { let buffer: AVAudioPCMBuffer }
|
private struct UnsafeBuffer: @unchecked Sendable { let buffer: AVAudioPCMBuffer }
|
||||||
|
|
||||||
|
|||||||
202
Swabble/Sources/SwabbleKit/WakeWordGate.swift
Normal file
202
Swabble/Sources/SwabbleKit/WakeWordGate.swift
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct WakeWordSegment: Sendable, Equatable {
|
||||||
|
public let text: String
|
||||||
|
public let start: TimeInterval
|
||||||
|
public let duration: TimeInterval
|
||||||
|
public let range: Range<String.Index>?
|
||||||
|
|
||||||
|
public init(text: String, start: TimeInterval, duration: TimeInterval, range: Range<String.Index>? = nil) {
|
||||||
|
self.text = text
|
||||||
|
self.start = start
|
||||||
|
self.duration = duration
|
||||||
|
self.range = range
|
||||||
|
}
|
||||||
|
|
||||||
|
public var end: TimeInterval { start + duration }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct WakeWordGateConfig: Sendable, Equatable {
|
||||||
|
public var triggers: [String]
|
||||||
|
public var minPostTriggerGap: TimeInterval
|
||||||
|
public var minCommandLength: Int
|
||||||
|
|
||||||
|
public init(
|
||||||
|
triggers: [String],
|
||||||
|
minPostTriggerGap: TimeInterval = 0.45,
|
||||||
|
minCommandLength: Int = 1)
|
||||||
|
{
|
||||||
|
self.triggers = triggers
|
||||||
|
self.minPostTriggerGap = minPostTriggerGap
|
||||||
|
self.minCommandLength = minCommandLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct WakeWordGateMatch: Sendable, Equatable {
|
||||||
|
public let triggerEndTime: TimeInterval
|
||||||
|
public let postGap: TimeInterval
|
||||||
|
public let command: String
|
||||||
|
|
||||||
|
public init(triggerEndTime: TimeInterval, postGap: TimeInterval, command: String) {
|
||||||
|
self.triggerEndTime = triggerEndTime
|
||||||
|
self.postGap = postGap
|
||||||
|
self.command = command
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum WakeWordGate {
|
||||||
|
private struct Token {
|
||||||
|
let normalized: String
|
||||||
|
let start: TimeInterval
|
||||||
|
let end: TimeInterval
|
||||||
|
let range: Range<String.Index>?
|
||||||
|
let text: String
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct TriggerTokens {
|
||||||
|
let tokens: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func match(
|
||||||
|
transcript: String,
|
||||||
|
segments: [WakeWordSegment],
|
||||||
|
config: WakeWordGateConfig)
|
||||||
|
-> WakeWordGateMatch? {
|
||||||
|
let triggerTokens = normalizeTriggers(config.triggers)
|
||||||
|
guard !triggerTokens.isEmpty else { return nil }
|
||||||
|
|
||||||
|
let tokens = normalizeSegments(segments)
|
||||||
|
guard !tokens.isEmpty else { return nil }
|
||||||
|
|
||||||
|
var bestIndex: Int?
|
||||||
|
var bestTriggerEnd: TimeInterval = 0
|
||||||
|
var bestGap: TimeInterval = 0
|
||||||
|
|
||||||
|
for trigger in triggerTokens {
|
||||||
|
let count = trigger.tokens.count
|
||||||
|
guard count > 0, tokens.count > count else { continue }
|
||||||
|
for i in 0...(tokens.count - count - 1) {
|
||||||
|
var matched = true
|
||||||
|
for t in 0..<count {
|
||||||
|
if tokens[i + t].normalized != trigger.tokens[t] {
|
||||||
|
matched = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched { continue }
|
||||||
|
|
||||||
|
let triggerEnd = tokens[i + count - 1].end
|
||||||
|
let nextToken = tokens[i + count]
|
||||||
|
let gap = nextToken.start - triggerEnd
|
||||||
|
if gap < config.minPostTriggerGap { continue }
|
||||||
|
|
||||||
|
if let bestIndex, i <= bestIndex { continue }
|
||||||
|
|
||||||
|
bestIndex = i
|
||||||
|
bestTriggerEnd = triggerEnd
|
||||||
|
bestGap = gap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let bestIndex else { return nil }
|
||||||
|
let command = commandText(transcript: transcript, segments: segments, triggerEndTime: bestTriggerEnd)
|
||||||
|
.trimmingCharacters(in: Self.whitespaceAndPunctuation)
|
||||||
|
guard command.count >= config.minCommandLength else { return nil }
|
||||||
|
return WakeWordGateMatch(triggerEndTime: bestTriggerEnd, postGap: bestGap, command: command)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func commandText(
|
||||||
|
transcript: String,
|
||||||
|
segments: [WakeWordSegment],
|
||||||
|
triggerEndTime: TimeInterval)
|
||||||
|
-> String {
|
||||||
|
let threshold = triggerEndTime + 0.001
|
||||||
|
for segment in segments where segment.start >= threshold {
|
||||||
|
if normalizeToken(segment.text).isEmpty { continue }
|
||||||
|
if let range = segment.range {
|
||||||
|
let slice = transcript[range.lowerBound...]
|
||||||
|
return String(slice).trimmingCharacters(in: Self.whitespaceAndPunctuation)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = segments
|
||||||
|
.filter { $0.start >= threshold && !normalizeToken($0.text).isEmpty }
|
||||||
|
.map { $0.text }
|
||||||
|
.joined(separator: " ")
|
||||||
|
return text.trimmingCharacters(in: Self.whitespaceAndPunctuation)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func matchesTextOnly(text: String, triggers: [String]) -> Bool {
|
||||||
|
guard !text.isEmpty else { return false }
|
||||||
|
let normalized = text.lowercased()
|
||||||
|
for trigger in triggers {
|
||||||
|
let token = trigger.trimmingCharacters(in: whitespaceAndPunctuation).lowercased()
|
||||||
|
if token.isEmpty { continue }
|
||||||
|
if normalized.contains(token) { return true }
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func stripWake(text: String, triggers: [String]) -> String {
|
||||||
|
var out = text
|
||||||
|
for trigger in triggers {
|
||||||
|
let token = trigger.trimmingCharacters(in: whitespaceAndPunctuation)
|
||||||
|
guard !token.isEmpty else { continue }
|
||||||
|
out = out.replacingOccurrences(of: token, with: "", options: [.caseInsensitive])
|
||||||
|
}
|
||||||
|
return out.trimmingCharacters(in: whitespaceAndPunctuation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func normalizeTriggers(_ triggers: [String]) -> [TriggerTokens] {
|
||||||
|
var output: [TriggerTokens] = []
|
||||||
|
for trigger in triggers {
|
||||||
|
let tokens = trigger
|
||||||
|
.split(whereSeparator: { $0.isWhitespace })
|
||||||
|
.map { normalizeToken(String($0)) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
if tokens.isEmpty { continue }
|
||||||
|
output.append(TriggerTokens(tokens: tokens))
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func normalizeSegments(_ segments: [WakeWordSegment]) -> [Token] {
|
||||||
|
segments.compactMap { segment in
|
||||||
|
let normalized = normalizeToken(segment.text)
|
||||||
|
guard !normalized.isEmpty else { return nil }
|
||||||
|
return Token(
|
||||||
|
normalized: normalized,
|
||||||
|
start: segment.start,
|
||||||
|
end: segment.end,
|
||||||
|
range: segment.range,
|
||||||
|
text: segment.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func normalizeToken(_ token: String) -> String {
|
||||||
|
token
|
||||||
|
.trimmingCharacters(in: whitespaceAndPunctuation)
|
||||||
|
.lowercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let whitespaceAndPunctuation = CharacterSet.whitespacesAndNewlines
|
||||||
|
.union(.punctuationCharacters)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(Speech)
|
||||||
|
import Speech
|
||||||
|
|
||||||
|
public enum WakeWordSpeechSegments {
|
||||||
|
public static func from(transcription: SFTranscription, transcript: String) -> [WakeWordSegment] {
|
||||||
|
transcription.segments.map { segment in
|
||||||
|
let range = Range(segment.substringRange, in: transcript)
|
||||||
|
return WakeWordSegment(
|
||||||
|
text: segment.substring,
|
||||||
|
start: segment.timestamp,
|
||||||
|
duration: segment.duration,
|
||||||
|
range: range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Commander
|
import Commander
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@available(macOS 26.0, *)
|
||||||
@MainActor
|
@MainActor
|
||||||
enum CLIRegistry {
|
enum CLIRegistry {
|
||||||
static var descriptors: [CommandDescriptor] {
|
static var descriptors: [CommandDescriptor] {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import Commander
|
import Commander
|
||||||
import Foundation
|
import Foundation
|
||||||
import Swabble
|
import Swabble
|
||||||
|
import SwabbleKit
|
||||||
|
|
||||||
|
@available(macOS 26.0, *)
|
||||||
@MainActor
|
@MainActor
|
||||||
struct ServeCommand: ParsableCommand {
|
struct ServeCommand: ParsableCommand {
|
||||||
@Option(name: .long("config"), help: "Path to config JSON") var configPath: String?
|
@Option(name: .long("config"), help: "Path to config JSON") var configPath: String?
|
||||||
@@ -68,17 +70,12 @@ struct ServeCommand: ParsableCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func matchesWake(text: String, cfg: SwabbleConfig) -> Bool {
|
private static func matchesWake(text: String, cfg: SwabbleConfig) -> Bool {
|
||||||
let lowered = text.lowercased()
|
let triggers = [cfg.wake.word] + cfg.wake.aliases
|
||||||
if lowered.contains(cfg.wake.word.lowercased()) { return true }
|
return WakeWordGate.matchesTextOnly(text: text, triggers: triggers)
|
||||||
return cfg.wake.aliases.contains(where: { lowered.contains($0.lowercased()) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func stripWake(text: String, cfg: SwabbleConfig) -> String {
|
private static func stripWake(text: String, cfg: SwabbleConfig) -> String {
|
||||||
var out = text
|
let triggers = [cfg.wake.word] + cfg.wake.aliases
|
||||||
out = out.replacingOccurrences(of: cfg.wake.word, with: "", options: [.caseInsensitive])
|
return WakeWordGate.stripWake(text: text, triggers: triggers)
|
||||||
for alias in cfg.wake.aliases {
|
|
||||||
out = out.replacingOccurrences(of: alias, with: "", options: [.caseInsensitive])
|
|
||||||
}
|
|
||||||
return out.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Commander
|
import Commander
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@available(macOS 26.0, *)
|
||||||
@MainActor
|
@MainActor
|
||||||
private func runCLI() async -> Int32 {
|
private func runCLI() async -> Int32 {
|
||||||
do {
|
do {
|
||||||
@@ -15,6 +16,7 @@ private func runCLI() async -> Int32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(macOS 26.0, *)
|
||||||
@MainActor
|
@MainActor
|
||||||
private func dispatch(invocation: CommandInvocation) async throws {
|
private func dispatch(invocation: CommandInvocation) async throws {
|
||||||
let parsed = invocation.parsedValues
|
let parsed = invocation.parsedValues
|
||||||
@@ -95,5 +97,10 @@ private func dispatch(invocation: CommandInvocation) async throws {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let exitCode = await runCLI()
|
if #available(macOS 26.0, *) {
|
||||||
exit(exitCode)
|
let exitCode = await runCLI()
|
||||||
|
exit(exitCode)
|
||||||
|
} else {
|
||||||
|
fputs("error: swabble requires macOS 26 or newer\n", stderr)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|||||||
63
Swabble/Tests/SwabbleKitTests/WakeWordGateTests.swift
Normal file
63
Swabble/Tests/SwabbleKitTests/WakeWordGateTests.swift
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
import SwabbleKit
|
||||||
|
|
||||||
|
@Suite struct WakeWordGateTests {
|
||||||
|
@Test func matchRequiresGapAfterTrigger() {
|
||||||
|
let transcript = "hey clawd do thing"
|
||||||
|
let segments = makeSegments(
|
||||||
|
transcript: transcript,
|
||||||
|
words: [
|
||||||
|
("hey", 0.0, 0.1),
|
||||||
|
("clawd", 0.2, 0.1),
|
||||||
|
("do", 0.35, 0.1),
|
||||||
|
("thing", 0.5, 0.1),
|
||||||
|
])
|
||||||
|
let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3)
|
||||||
|
#expect(WakeWordGate.match(transcript: transcript, segments: segments, config: config) == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func matchAllowsGapAndExtractsCommand() {
|
||||||
|
let transcript = "hey clawd do thing"
|
||||||
|
let segments = makeSegments(
|
||||||
|
transcript: transcript,
|
||||||
|
words: [
|
||||||
|
("hey", 0.0, 0.1),
|
||||||
|
("clawd", 0.2, 0.1),
|
||||||
|
("do", 0.9, 0.1),
|
||||||
|
("thing", 1.1, 0.1),
|
||||||
|
])
|
||||||
|
let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3)
|
||||||
|
let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config)
|
||||||
|
#expect(match?.command == "do thing")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func matchHandlesMultiWordTriggers() {
|
||||||
|
let transcript = "hey clawd do it"
|
||||||
|
let segments = makeSegments(
|
||||||
|
transcript: transcript,
|
||||||
|
words: [
|
||||||
|
("hey", 0.0, 0.1),
|
||||||
|
("clawd", 0.2, 0.1),
|
||||||
|
("do", 0.8, 0.1),
|
||||||
|
("it", 1.0, 0.1),
|
||||||
|
])
|
||||||
|
let config = WakeWordGateConfig(triggers: ["hey clawd"], minPostTriggerGap: 0.3)
|
||||||
|
let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config)
|
||||||
|
#expect(match?.command == "do it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeSegments(
|
||||||
|
transcript: String,
|
||||||
|
words: [(String, TimeInterval, TimeInterval)])
|
||||||
|
-> [WakeWordSegment] {
|
||||||
|
var searchStart = transcript.startIndex
|
||||||
|
var output: [WakeWordSegment] = []
|
||||||
|
for (word, start, duration) in words {
|
||||||
|
let range = transcript.range(of: word, range: searchStart..<transcript.endIndex)
|
||||||
|
output.append(WakeWordSegment(text: word, start: start, duration: duration, range: range))
|
||||||
|
if let range { searchStart = range.upperBound }
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
# swabble — macOS 26 speech hook daemon (Swift 6.2)
|
# swabble — macOS 26 speech hook daemon (Swift 6.2)
|
||||||
|
|
||||||
Goal: brabble-style always-on voice hook for macOS 26 using Apple Speech.framework (SpeechAnalyzer + SpeechTranscriber) instead of whisper.cpp. Local-only, wake word gated, dispatches a shell hook with the transcript.
|
Goal: brabble-style always-on voice hook for macOS 26 using Apple Speech.framework (SpeechAnalyzer + SpeechTranscriber) instead of whisper.cpp. Local-only, wake word gated, dispatches a shell hook with the transcript. Shared wake-gate utilities live in `SwabbleKit` for reuse by other apps (iOS/macOS).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- macOS 26+, Swift 6.2, Speech.framework with on-device assets.
|
- macOS 26+, Swift 6.2, Speech.framework with on-device assets.
|
||||||
- Local only; no network calls during transcription.
|
- Local only; no network calls during transcription.
|
||||||
- Wake word gating (default "clawd" plus aliases) with bypass flag `--no-wake`.
|
- Wake word gating (default "clawd" plus aliases) with bypass flag `--no-wake`.
|
||||||
|
- `SwabbleKit` target (multi-platform) providing wake-word gating helpers that can use speech segment timing to require a post-trigger gap.
|
||||||
- Hook execution with cooldown, min_chars, timeout, prefix, env vars.
|
- Hook execution with cooldown, min_chars, timeout, prefix, env vars.
|
||||||
- Simple config at `~/.config/swabble/config.json` (JSON, Codable) — no TOML.
|
- Simple config at `~/.config/swabble/config.json` (JSON, Codable) — no TOML.
|
||||||
- CLI implemented with Commander (SwiftPM package `steipete/Commander`); core types are available via the SwiftPM library product `Swabble` for embedding.
|
- CLI implemented with Commander (SwiftPM package `steipete/Commander`); core types are available via the SwiftPM library product `Swabble` for embedding.
|
||||||
@@ -17,7 +18,7 @@ Goal: brabble-style always-on voice hook for macOS 26 using Apple Speech.framewo
|
|||||||
- **CLI layer (Commander)**: Root command `swabble` with subcommands `serve`, `transcribe`, `test-hook`, `mic list|set`, `doctor`, `health`, `tail-log`. Runtime flags from Commander (`-v/--verbose`, `--json-output`, `--log-level`). Custom `--config` path applies everywhere.
|
- **CLI layer (Commander)**: Root command `swabble` with subcommands `serve`, `transcribe`, `test-hook`, `mic list|set`, `doctor`, `health`, `tail-log`. Runtime flags from Commander (`-v/--verbose`, `--json-output`, `--log-level`). Custom `--config` path applies everywhere.
|
||||||
- **Config**: `SwabbleConfig` Codable. Fields: audio device name/index, wake (enabled/word/aliases/sensitivity placeholder), hook (command/args/prefix/cooldown/min_chars/timeout/env), logging (level, format), transcripts (enabled, max kept), speech (locale, enableEtiquetteReplacements flag). Stored JSON; default written by `setup`.
|
- **Config**: `SwabbleConfig` Codable. Fields: audio device name/index, wake (enabled/word/aliases/sensitivity placeholder), hook (command/args/prefix/cooldown/min_chars/timeout/env), logging (level, format), transcripts (enabled, max kept), speech (locale, enableEtiquetteReplacements flag). Stored JSON; default written by `setup`.
|
||||||
- **Audio + Speech pipeline**: `SpeechPipeline` wraps `AVAudioEngine` input → `SpeechAnalyzer` with `SpeechTranscriber` module. Emits partial/final transcripts via async stream. Requests `.audioTimeRange` when transcripts enabled. Handles Speech permission and asset download prompts ahead of capture.
|
- **Audio + Speech pipeline**: `SpeechPipeline` wraps `AVAudioEngine` input → `SpeechAnalyzer` with `SpeechTranscriber` module. Emits partial/final transcripts via async stream. Requests `.audioTimeRange` when transcripts enabled. Handles Speech permission and asset download prompts ahead of capture.
|
||||||
- **Wake gate**: text-based keyword match against latest partial/final; strips wake term before hook dispatch. `--no-wake` disables.
|
- **Wake gate**: CLI currently uses text-only keyword match; shared `SwabbleKit` gate can enforce a minimum pause between the wake word and the next token when speech segments are available. `--no-wake` disables gating.
|
||||||
- **Hook executor**: async `HookExecutor` spawns `Process` with configured args, prefix substitution `${hostname}`. Enforces cooldown + timeout; injects env `SWABBLE_TEXT`, `SWABBLE_PREFIX` plus user env map.
|
- **Hook executor**: async `HookExecutor` spawns `Process` with configured args, prefix substitution `${hostname}`. Enforces cooldown + timeout; injects env `SWABBLE_TEXT`, `SWABBLE_PREFIX` plus user env map.
|
||||||
- **Transcripts store**: in-memory ring buffer; optional persisted JSON lines under `~/Library/Application Support/swabble/transcripts.log`.
|
- **Transcripts store**: in-memory ring buffer; optional persisted JSON lines under `~/Library/Application Support/swabble/transcripts.log`.
|
||||||
- **Logging**: simple structured logger to stderr; respects log level.
|
- **Logging**: simple structured logger to stderr; respects log level.
|
||||||
@@ -25,7 +26,7 @@ Goal: brabble-style always-on voice hook for macOS 26 using Apple Speech.framewo
|
|||||||
## Out of scope (initial cut)
|
## Out of scope (initial cut)
|
||||||
- Model management (Speech handles assets).
|
- Model management (Speech handles assets).
|
||||||
- Launchd helper (planned follow-up).
|
- Launchd helper (planned follow-up).
|
||||||
- Advanced wake-word detector (text match only for now).
|
- Advanced wake-word detector (segment-aware gate now lives in `SwabbleKit`; CLI still text-only until segment timing is plumbed through).
|
||||||
|
|
||||||
## Open decisions
|
## Open decisions
|
||||||
- Whether to expose a UNIX control socket for `status`/`health` (currently planned as stdin/out direct calls).
|
- Whether to expose a UNIX control socket for `status`/`health` (currently planned as stdin/out direct calls).
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import AVFAudio
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
import Speech
|
import Speech
|
||||||
|
import SwabbleKit
|
||||||
|
|
||||||
private func makeAudioTapEnqueueCallback(queue: AudioBufferQueue) -> @Sendable (AVAudioPCMBuffer, AVAudioTime) -> Void {
|
private func makeAudioTapEnqueueCallback(queue: AudioBufferQueue) -> @Sendable (AVAudioPCMBuffer, AVAudioTime) -> Void {
|
||||||
{ buffer, _ in
|
{ buffer, _ in
|
||||||
@@ -289,15 +290,18 @@ final class VoiceWakeManager: NSObject {
|
|||||||
private nonisolated func makeRecognitionResultHandler() -> @Sendable (SFSpeechRecognitionResult?, Error?) -> Void {
|
private nonisolated func makeRecognitionResultHandler() -> @Sendable (SFSpeechRecognitionResult?, Error?) -> Void {
|
||||||
{ [weak self] result, error in
|
{ [weak self] result, error in
|
||||||
let transcript = result?.bestTranscription.formattedString
|
let transcript = result?.bestTranscription.formattedString
|
||||||
|
let segments = result.flatMap { result in
|
||||||
|
transcript.map { WakeWordSpeechSegments.from(transcription: result.bestTranscription, transcript: $0) }
|
||||||
|
} ?? []
|
||||||
let errorText = error?.localizedDescription
|
let errorText = error?.localizedDescription
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self?.handleRecognitionCallback(transcript: transcript, errorText: errorText)
|
self?.handleRecognitionCallback(transcript: transcript, segments: segments, errorText: errorText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleRecognitionCallback(transcript: String?, errorText: String?) {
|
private func handleRecognitionCallback(transcript: String?, segments: [WakeWordSegment], errorText: String?) {
|
||||||
if let errorText {
|
if let errorText {
|
||||||
self.statusText = "Recognizer error: \(errorText)"
|
self.statusText = "Recognizer error: \(errorText)"
|
||||||
self.isListening = false
|
self.isListening = false
|
||||||
@@ -313,7 +317,7 @@ final class VoiceWakeManager: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard let transcript else { return }
|
guard let transcript else { return }
|
||||||
guard let cmd = self.extractCommand(from: transcript) else { return }
|
guard let cmd = self.extractCommand(from: transcript, segments: segments) else { return }
|
||||||
|
|
||||||
if cmd == self.lastDispatched { return }
|
if cmd == self.lastDispatched { return }
|
||||||
self.lastDispatched = cmd
|
self.lastDispatched = cmd
|
||||||
@@ -334,30 +338,18 @@ final class VoiceWakeManager: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func extractCommand(from transcript: String) -> String? {
|
private func extractCommand(from transcript: String, segments: [WakeWordSegment]) -> String? {
|
||||||
Self.extractCommand(from: transcript, triggers: self.activeTriggerWords)
|
Self.extractCommand(from: transcript, segments: segments, triggers: self.activeTriggerWords)
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated static func extractCommand(from transcript: String, triggers: [String]) -> String? {
|
nonisolated static func extractCommand(
|
||||||
var bestRange: Range<String.Index>?
|
from transcript: String,
|
||||||
for trigger in triggers {
|
segments: [WakeWordSegment],
|
||||||
let token = trigger.trimmingCharacters(in: .whitespacesAndNewlines)
|
triggers: [String],
|
||||||
guard !token.isEmpty else { continue }
|
minPostTriggerGap: TimeInterval = 0.45) -> String?
|
||||||
guard let range = transcript.range(of: token, options: [.caseInsensitive, .backwards]) else { continue }
|
{
|
||||||
if let currentBest = bestRange {
|
let config = WakeWordGateConfig(triggers: triggers, minPostTriggerGap: minPostTriggerGap)
|
||||||
if range.lowerBound > currentBest.lowerBound {
|
return WakeWordGate.match(transcript: transcript, segments: segments, config: config)?.command
|
||||||
bestRange = range
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bestRange = range
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let bestRange else { return nil }
|
|
||||||
let after = transcript[bestRange.upperBound...]
|
|
||||||
let trimmed = after.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
guard !trimmed.isEmpty else { return nil }
|
|
||||||
return String(trimmed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func configureAudioSession() throws {
|
private static func configureAudioSession() throws {
|
||||||
|
|||||||
@@ -54,3 +54,4 @@ Sources/Voice/VoiceWakePreferences.swift
|
|||||||
../shared/ClawdisKit/Sources/ClawdisKit/ScreenCommands.swift
|
../shared/ClawdisKit/Sources/ClawdisKit/ScreenCommands.swift
|
||||||
../shared/ClawdisKit/Sources/ClawdisKit/StoragePaths.swift
|
../shared/ClawdisKit/Sources/ClawdisKit/StoragePaths.swift
|
||||||
../shared/ClawdisKit/Sources/ClawdisKit/SystemCommands.swift
|
../shared/ClawdisKit/Sources/ClawdisKit/SystemCommands.swift
|
||||||
|
../../Swabble/Sources/SwabbleKit/WakeWordGate.swift
|
||||||
|
|||||||
@@ -1,33 +1,90 @@
|
|||||||
|
import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
|
import SwabbleKit
|
||||||
@testable import Clawdis
|
@testable import Clawdis
|
||||||
|
|
||||||
@Suite struct VoiceWakeManagerExtractCommandTests {
|
@Suite struct VoiceWakeManagerExtractCommandTests {
|
||||||
@Test func extractCommandReturnsNilWhenNoTriggerFound() {
|
@Test func extractCommandReturnsNilWhenNoTriggerFound() {
|
||||||
#expect(VoiceWakeManager.extractCommand(from: "hello world", triggers: ["clawd"]) == nil)
|
let transcript = "hello world"
|
||||||
|
let segments = makeSegments(
|
||||||
|
transcript: transcript,
|
||||||
|
words: [("hello", 0.0, 0.1), ("world", 0.2, 0.1)])
|
||||||
|
#expect(VoiceWakeManager.extractCommand(from: transcript, segments: segments, triggers: ["clawd"]) == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func extractCommandTrimsTokensAndResult() {
|
@Test func extractCommandTrimsTokensAndResult() {
|
||||||
let cmd = VoiceWakeManager.extractCommand(from: "hey clawd do thing ", triggers: [" clawd "])
|
let transcript = "hey clawd do thing"
|
||||||
|
let segments = makeSegments(
|
||||||
|
transcript: transcript,
|
||||||
|
words: [
|
||||||
|
("hey", 0.0, 0.1),
|
||||||
|
("clawd", 0.2, 0.1),
|
||||||
|
("do", 0.9, 0.1),
|
||||||
|
("thing", 1.1, 0.1),
|
||||||
|
])
|
||||||
|
let cmd = VoiceWakeManager.extractCommand(
|
||||||
|
from: transcript,
|
||||||
|
segments: segments,
|
||||||
|
triggers: [" clawd "],
|
||||||
|
minPostTriggerGap: 0.3)
|
||||||
#expect(cmd == "do thing")
|
#expect(cmd == "do thing")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func extractCommandPicksLatestTriggerOccurrence() {
|
@Test func extractCommandReturnsNilWhenGapTooShort() {
|
||||||
let transcript = "clawd first\nthen something\nclaude second"
|
let transcript = "hey clawd do thing"
|
||||||
let cmd = VoiceWakeManager.extractCommand(from: transcript, triggers: ["clawd", "claude"])
|
let segments = makeSegments(
|
||||||
#expect(cmd == "second")
|
transcript: transcript,
|
||||||
}
|
words: [
|
||||||
|
("hey", 0.0, 0.1),
|
||||||
@Test func extractCommandIsCaseInsensitive() {
|
("clawd", 0.2, 0.1),
|
||||||
let cmd = VoiceWakeManager.extractCommand(from: "HELLO CLAWD run it", triggers: ["clawd"])
|
("do", 0.35, 0.1),
|
||||||
#expect(cmd == "run it")
|
("thing", 0.5, 0.1),
|
||||||
|
])
|
||||||
|
let cmd = VoiceWakeManager.extractCommand(
|
||||||
|
from: transcript,
|
||||||
|
segments: segments,
|
||||||
|
triggers: ["clawd"],
|
||||||
|
minPostTriggerGap: 0.3)
|
||||||
|
#expect(cmd == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func extractCommandReturnsNilWhenNothingAfterTrigger() {
|
@Test func extractCommandReturnsNilWhenNothingAfterTrigger() {
|
||||||
#expect(VoiceWakeManager.extractCommand(from: "hey clawd \n", triggers: ["clawd"]) == nil)
|
let transcript = "hey clawd"
|
||||||
|
let segments = makeSegments(
|
||||||
|
transcript: transcript,
|
||||||
|
words: [("hey", 0.0, 0.1), ("clawd", 0.2, 0.1)])
|
||||||
|
#expect(VoiceWakeManager.extractCommand(from: transcript, segments: segments, triggers: ["clawd"]) == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func extractCommandIgnoresEmptyTriggers() {
|
@Test func extractCommandIgnoresEmptyTriggers() {
|
||||||
let cmd = VoiceWakeManager.extractCommand(from: "hey clawd do thing", triggers: ["", " ", "clawd"])
|
let transcript = "hey clawd do thing"
|
||||||
|
let segments = makeSegments(
|
||||||
|
transcript: transcript,
|
||||||
|
words: [
|
||||||
|
("hey", 0.0, 0.1),
|
||||||
|
("clawd", 0.2, 0.1),
|
||||||
|
("do", 0.9, 0.1),
|
||||||
|
("thing", 1.1, 0.1),
|
||||||
|
])
|
||||||
|
let cmd = VoiceWakeManager.extractCommand(
|
||||||
|
from: transcript,
|
||||||
|
segments: segments,
|
||||||
|
triggers: ["", " ", "clawd"],
|
||||||
|
minPostTriggerGap: 0.3)
|
||||||
#expect(cmd == "do thing")
|
#expect(cmd == "do thing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func makeSegments(
|
||||||
|
transcript: String,
|
||||||
|
words: [(String, TimeInterval, TimeInterval)])
|
||||||
|
-> [WakeWordSegment] {
|
||||||
|
var searchStart = transcript.startIndex
|
||||||
|
var output: [WakeWordSegment] = []
|
||||||
|
for (word, start, duration) in words {
|
||||||
|
let range = transcript.range(of: word, range: searchStart..<transcript.endIndex)
|
||||||
|
output.append(WakeWordSegment(text: word, start: start, duration: duration, range: range))
|
||||||
|
if let range { searchStart = range.upperBound }
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ options:
|
|||||||
packages:
|
packages:
|
||||||
ClawdisKit:
|
ClawdisKit:
|
||||||
path: ../shared/ClawdisKit
|
path: ../shared/ClawdisKit
|
||||||
|
Swabble:
|
||||||
|
path: ../../Swabble
|
||||||
|
|
||||||
schemes:
|
schemes:
|
||||||
Clawdis:
|
Clawdis:
|
||||||
@@ -29,6 +31,8 @@ targets:
|
|||||||
- package: ClawdisKit
|
- package: ClawdisKit
|
||||||
- package: ClawdisKit
|
- package: ClawdisKit
|
||||||
product: ClawdisChatUI
|
product: ClawdisChatUI
|
||||||
|
- package: Swabble
|
||||||
|
product: SwabbleKit
|
||||||
- sdk: AppIntents.framework
|
- sdk: AppIntents.framework
|
||||||
preBuildScripts:
|
preBuildScripts:
|
||||||
- name: SwiftFormat (lint)
|
- name: SwiftFormat (lint)
|
||||||
@@ -86,6 +90,8 @@ targets:
|
|||||||
- path: Tests
|
- path: Tests
|
||||||
dependencies:
|
dependencies:
|
||||||
- target: Clawdis
|
- target: Clawdis
|
||||||
|
- package: Swabble
|
||||||
|
product: SwabbleKit
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: com.steipete.clawdis.ios.tests
|
PRODUCT_BUNDLE_IDENTIFIER: com.steipete.clawdis.ios.tests
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ let package = Package(
|
|||||||
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.1.0"),
|
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.1.0"),
|
||||||
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.8.1"),
|
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.8.1"),
|
||||||
.package(path: "../shared/ClawdisKit"),
|
.package(path: "../shared/ClawdisKit"),
|
||||||
|
.package(path: "../../Swabble"),
|
||||||
.package(path: "../../Peekaboo/Core/PeekabooCore"),
|
.package(path: "../../Peekaboo/Core/PeekabooCore"),
|
||||||
.package(path: "../../Peekaboo/Core/PeekabooAutomationKit"),
|
.package(path: "../../Peekaboo/Core/PeekabooAutomationKit"),
|
||||||
],
|
],
|
||||||
@@ -41,6 +42,7 @@ let package = Package(
|
|||||||
"ClawdisProtocol",
|
"ClawdisProtocol",
|
||||||
.product(name: "ClawdisKit", package: "ClawdisKit"),
|
.product(name: "ClawdisKit", package: "ClawdisKit"),
|
||||||
.product(name: "ClawdisChatUI", package: "ClawdisKit"),
|
.product(name: "ClawdisChatUI", package: "ClawdisKit"),
|
||||||
|
.product(name: "SwabbleKit", package: "swabble"),
|
||||||
.product(name: "MenuBarExtraAccess", package: "MenuBarExtraAccess"),
|
.product(name: "MenuBarExtraAccess", package: "MenuBarExtraAccess"),
|
||||||
.product(name: "Subprocess", package: "swift-subprocess"),
|
.product(name: "Subprocess", package: "swift-subprocess"),
|
||||||
.product(name: "Sparkle", package: "Sparkle"),
|
.product(name: "Sparkle", package: "Sparkle"),
|
||||||
@@ -56,7 +58,12 @@ let package = Package(
|
|||||||
]),
|
]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "ClawdisIPCTests",
|
name: "ClawdisIPCTests",
|
||||||
dependencies: ["ClawdisIPC", "Clawdis", "ClawdisProtocol"],
|
dependencies: [
|
||||||
|
"ClawdisIPC",
|
||||||
|
"Clawdis",
|
||||||
|
"ClawdisProtocol",
|
||||||
|
.product(name: "SwabbleKit", package: "swabble"),
|
||||||
|
],
|
||||||
swiftSettings: [
|
swiftSettings: [
|
||||||
.enableUpcomingFeature("StrictConcurrency"),
|
.enableUpcomingFeature("StrictConcurrency"),
|
||||||
.enableExperimentalFeature("SwiftTesting"),
|
.enableExperimentalFeature("SwiftTesting"),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import AVFoundation
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
import Speech
|
import Speech
|
||||||
|
import SwabbleKit
|
||||||
#if canImport(AppKit)
|
#if canImport(AppKit)
|
||||||
import AppKit
|
import AppKit
|
||||||
#endif
|
#endif
|
||||||
@@ -35,6 +36,7 @@ actor VoiceWakeRuntime {
|
|||||||
private var currentConfig: RuntimeConfig?
|
private var currentConfig: RuntimeConfig?
|
||||||
private var listeningState: ListeningState = .idle
|
private var listeningState: ListeningState = .idle
|
||||||
private var overlayToken: UUID?
|
private var overlayToken: UUID?
|
||||||
|
private var activeTriggerEndTime: TimeInterval?
|
||||||
|
|
||||||
// Tunables
|
// Tunables
|
||||||
// Silence threshold once we've captured user speech (post-trigger).
|
// Silence threshold once we've captured user speech (post-trigger).
|
||||||
@@ -147,9 +149,13 @@ actor VoiceWakeRuntime {
|
|||||||
self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self, generation] result, error in
|
self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self, generation] result, error in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
let transcript = result?.bestTranscription.formattedString
|
let transcript = result?.bestTranscription.formattedString
|
||||||
|
let segments = result.flatMap { result in
|
||||||
|
transcript.map { WakeWordSpeechSegments.from(transcription: result.bestTranscription, transcript: $0) }
|
||||||
|
} ?? []
|
||||||
let isFinal = result?.isFinal ?? false
|
let isFinal = result?.isFinal ?? false
|
||||||
Task { await self.handleRecognition(
|
Task { await self.handleRecognition(
|
||||||
transcript: transcript,
|
transcript: transcript,
|
||||||
|
segments: segments,
|
||||||
isFinal: isFinal,
|
isFinal: isFinal,
|
||||||
error: error,
|
error: error,
|
||||||
config: config,
|
config: config,
|
||||||
@@ -184,6 +190,7 @@ actor VoiceWakeRuntime {
|
|||||||
self.audioEngine = nil
|
self.audioEngine = nil
|
||||||
self.currentConfig = nil
|
self.currentConfig = nil
|
||||||
self.listeningState = .idle
|
self.listeningState = .idle
|
||||||
|
self.activeTriggerEndTime = nil
|
||||||
self.logger.debug("voicewake runtime stopped")
|
self.logger.debug("voicewake runtime stopped")
|
||||||
DiagnosticsFileLog.shared.log(category: "voicewake.runtime", event: "stopped")
|
DiagnosticsFileLog.shared.log(category: "voicewake.runtime", event: "stopped")
|
||||||
|
|
||||||
@@ -206,6 +213,7 @@ actor VoiceWakeRuntime {
|
|||||||
|
|
||||||
private func handleRecognition(
|
private func handleRecognition(
|
||||||
transcript: String?,
|
transcript: String?,
|
||||||
|
segments: [WakeWordSegment],
|
||||||
isFinal: Bool,
|
isFinal: Bool,
|
||||||
error: Error?,
|
error: Error?,
|
||||||
config: RuntimeConfig,
|
config: RuntimeConfig,
|
||||||
@@ -224,7 +232,11 @@ actor VoiceWakeRuntime {
|
|||||||
if !transcript.isEmpty {
|
if !transcript.isEmpty {
|
||||||
self.lastHeard = now
|
self.lastHeard = now
|
||||||
if self.isCapturing {
|
if self.isCapturing {
|
||||||
let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers)
|
let trimmed = Self.commandAfterTrigger(
|
||||||
|
transcript: transcript,
|
||||||
|
segments: segments,
|
||||||
|
triggerEndTime: self.activeTriggerEndTime,
|
||||||
|
triggers: config.triggers)
|
||||||
self.capturedTranscript = trimmed
|
self.capturedTranscript = trimmed
|
||||||
self.updateHeardBeyondTrigger(withTrimmed: trimmed)
|
self.updateHeardBeyondTrigger(withTrimmed: trimmed)
|
||||||
if isFinal {
|
if isFinal {
|
||||||
@@ -252,37 +264,27 @@ actor VoiceWakeRuntime {
|
|||||||
|
|
||||||
if self.isCapturing { return }
|
if self.isCapturing { return }
|
||||||
|
|
||||||
if Self.matches(text: transcript, triggers: config.triggers) {
|
let gateConfig = WakeWordGateConfig(triggers: config.triggers)
|
||||||
|
if let match = WakeWordGate.match(transcript: transcript, segments: segments, config: gateConfig) {
|
||||||
if let cooldown = cooldownUntil, now < cooldown {
|
if let cooldown = cooldownUntil, now < cooldown {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await self.beginCapture(transcript: transcript, config: config)
|
await self.beginCapture(command: match.command, triggerEndTime: match.triggerEndTime, config: config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func matches(text: String, triggers: [String]) -> Bool {
|
private func beginCapture(command: String, triggerEndTime: TimeInterval, config: RuntimeConfig) async {
|
||||||
guard !text.isEmpty else { return false }
|
|
||||||
let normalized = text.lowercased()
|
|
||||||
for trigger in triggers {
|
|
||||||
let t = trigger.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if t.isEmpty { continue }
|
|
||||||
if normalized.contains(t) { return true }
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func beginCapture(transcript: String, config: RuntimeConfig) async {
|
|
||||||
self.listeningState = .voiceWake
|
self.listeningState = .voiceWake
|
||||||
self.isCapturing = true
|
self.isCapturing = true
|
||||||
DiagnosticsFileLog.shared.log(category: "voicewake.runtime", event: "beginCapture")
|
DiagnosticsFileLog.shared.log(category: "voicewake.runtime", event: "beginCapture")
|
||||||
let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers)
|
self.capturedTranscript = command
|
||||||
self.capturedTranscript = trimmed
|
|
||||||
self.committedTranscript = ""
|
self.committedTranscript = ""
|
||||||
self.volatileTranscript = trimmed
|
self.volatileTranscript = command
|
||||||
self.captureStartedAt = Date()
|
self.captureStartedAt = Date()
|
||||||
self.cooldownUntil = nil
|
self.cooldownUntil = nil
|
||||||
self.heardBeyondTrigger = !trimmed.isEmpty
|
self.heardBeyondTrigger = !command.isEmpty
|
||||||
self.triggerChimePlayed = false
|
self.triggerChimePlayed = false
|
||||||
|
self.activeTriggerEndTime = triggerEndTime
|
||||||
|
|
||||||
if config.triggerChime != .none, !self.triggerChimePlayed {
|
if config.triggerChime != .none, !self.triggerChimePlayed {
|
||||||
self.triggerChimePlayed = true
|
self.triggerChimePlayed = true
|
||||||
@@ -354,6 +356,7 @@ actor VoiceWakeRuntime {
|
|||||||
self.lastHeard = nil
|
self.lastHeard = nil
|
||||||
self.heardBeyondTrigger = false
|
self.heardBeyondTrigger = false
|
||||||
self.triggerChimePlayed = false
|
self.triggerChimePlayed = false
|
||||||
|
self.activeTriggerEndTime = nil
|
||||||
|
|
||||||
await MainActor.run { AppStateStore.shared.stopVoiceEars() }
|
await MainActor.run { AppStateStore.shared.stopVoiceEars() }
|
||||||
if let token = self.overlayToken {
|
if let token = self.overlayToken {
|
||||||
@@ -467,6 +470,22 @@ actor VoiceWakeRuntime {
|
|||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func commandAfterTrigger(
|
||||||
|
transcript: String,
|
||||||
|
segments: [WakeWordSegment],
|
||||||
|
triggerEndTime: TimeInterval?,
|
||||||
|
triggers: [String]) -> String
|
||||||
|
{
|
||||||
|
guard let triggerEndTime else {
|
||||||
|
return trimmedAfterTrigger(transcript, triggers: triggers)
|
||||||
|
}
|
||||||
|
let trimmed = WakeWordGate.commandText(
|
||||||
|
transcript: transcript,
|
||||||
|
segments: segments,
|
||||||
|
triggerEndTime: triggerEndTime)
|
||||||
|
return trimmed.isEmpty ? trimmedAfterTrigger(transcript, triggers: triggers) : trimmed
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
static func _testTrimmedAfterTrigger(_ text: String, triggers: [String]) -> String {
|
static func _testTrimmedAfterTrigger(_ text: String, triggers: [String]) -> String {
|
||||||
self.trimmedAfterTrigger(text, triggers: triggers)
|
self.trimmedAfterTrigger(text, triggers: triggers)
|
||||||
@@ -481,9 +500,6 @@ actor VoiceWakeRuntime {
|
|||||||
.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? NSColor ?? .clear
|
.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? NSColor ?? .clear
|
||||||
}
|
}
|
||||||
|
|
||||||
static func _testMatches(text: String, triggers: [String]) -> Bool {
|
|
||||||
self.matches(text: text, triggers: triggers)
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private static func delta(after committed: String, current: String) -> String {
|
private static func delta(after committed: String, current: String) -> String {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import AVFoundation
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
import Speech
|
import Speech
|
||||||
|
import SwabbleKit
|
||||||
|
|
||||||
enum VoiceWakeTestState: Equatable {
|
enum VoiceWakeTestState: Equatable {
|
||||||
case idle
|
case idle
|
||||||
@@ -93,14 +94,16 @@ final class VoiceWakeTester {
|
|||||||
self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in
|
self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in
|
||||||
guard let self, !self.isStopping else { return }
|
guard let self, !self.isStopping else { return }
|
||||||
let text = result?.bestTranscription.formattedString ?? ""
|
let text = result?.bestTranscription.formattedString ?? ""
|
||||||
let matched = Self.matches(text: text, triggers: triggers)
|
let segments = result.map { WakeWordSpeechSegments.from(transcription: $0.bestTranscription, transcript: text) } ?? []
|
||||||
|
let gateConfig = WakeWordGateConfig(triggers: triggers)
|
||||||
|
let match = WakeWordGate.match(transcript: text, segments: segments, config: gateConfig)
|
||||||
let isFinal = result?.isFinal ?? false
|
let isFinal = result?.isFinal ?? false
|
||||||
let errorMessage = error?.localizedDescription
|
let errorMessage = error?.localizedDescription
|
||||||
|
|
||||||
Task { [weak self] in
|
Task { [weak self] in
|
||||||
guard let self, !self.isStopping else { return }
|
guard let self, !self.isStopping else { return }
|
||||||
await self.handleResult(
|
await self.handleResult(
|
||||||
matched: matched,
|
match: match,
|
||||||
text: text,
|
text: text,
|
||||||
isFinal: isFinal,
|
isFinal: isFinal,
|
||||||
errorMessage: errorMessage,
|
errorMessage: errorMessage,
|
||||||
@@ -120,7 +123,7 @@ final class VoiceWakeTester {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleResult(
|
private func handleResult(
|
||||||
matched: Bool,
|
match: WakeWordGateMatch?,
|
||||||
text: String,
|
text: String,
|
||||||
isFinal: Bool,
|
isFinal: Bool,
|
||||||
errorMessage: String?,
|
errorMessage: String?,
|
||||||
@@ -129,15 +132,15 @@ final class VoiceWakeTester {
|
|||||||
if !text.isEmpty {
|
if !text.isEmpty {
|
||||||
self.lastHeard = Date()
|
self.lastHeard = Date()
|
||||||
}
|
}
|
||||||
if matched, !text.isEmpty {
|
if let match, !match.command.isEmpty {
|
||||||
self.holdingAfterDetect = true
|
self.holdingAfterDetect = true
|
||||||
self.detectedText = text
|
self.detectedText = match.command
|
||||||
self.logger.info("voice wake detected; forwarding (len=\(text.count))")
|
self.logger.info("voice wake detected; forwarding (len=\(match.command.count))")
|
||||||
await MainActor.run { AppStateStore.shared.triggerVoiceEars(ttl: nil) }
|
await MainActor.run { AppStateStore.shared.triggerVoiceEars(ttl: nil) }
|
||||||
Task.detached {
|
Task.detached {
|
||||||
await VoiceWakeForwarder.forward(transcript: text)
|
await VoiceWakeForwarder.forward(transcript: match.command)
|
||||||
}
|
}
|
||||||
Task { @MainActor in onUpdate(.detected(text)) }
|
Task { @MainActor in onUpdate(.detected(match.command)) }
|
||||||
self.holdUntilSilence(onUpdate: onUpdate)
|
self.holdUntilSilence(onUpdate: onUpdate)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -187,15 +190,6 @@ final class VoiceWakeTester {
|
|||||||
_ = preferredMicID
|
_ = preferredMicID
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func matches(text: String, triggers: [String]) -> Bool {
|
|
||||||
let lowered = text.lowercased()
|
|
||||||
return triggers.contains { lowered.contains($0.lowercased()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
static func _testMatches(text: String, triggers: [String]) -> Bool {
|
|
||||||
self.matches(text: text, triggers: triggers)
|
|
||||||
}
|
|
||||||
|
|
||||||
private nonisolated static func ensurePermissions() async throws -> Bool {
|
private nonisolated static func ensurePermissions() async throws -> Bool {
|
||||||
let speechStatus = SFSpeechRecognizer.authorizationStatus()
|
let speechStatus = SFSpeechRecognizer.authorizationStatus()
|
||||||
if speechStatus == .notDetermined {
|
if speechStatus == .notDetermined {
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
|
import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
|
import SwabbleKit
|
||||||
@testable import Clawdis
|
@testable import Clawdis
|
||||||
|
|
||||||
@Suite struct VoiceWakeRuntimeTests {
|
@Suite struct VoiceWakeRuntimeTests {
|
||||||
@Test func matchesIsCaseInsensitive() {
|
|
||||||
let triggers = ["ClAwD", "buddy"]
|
|
||||||
#expect(VoiceWakeRuntime._testMatches(text: "hey clawd are you there", triggers: triggers))
|
|
||||||
#expect(!VoiceWakeRuntime._testMatches(text: "nothing to see", triggers: triggers))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func matchesIgnoresWhitespace() {
|
|
||||||
let triggers = [" claude "]
|
|
||||||
#expect(VoiceWakeRuntime._testMatches(text: "hello claude!", triggers: triggers))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func matchesSkipsEmptyTriggers() {
|
|
||||||
let triggers = [" ", ""]
|
|
||||||
#expect(!VoiceWakeRuntime._testMatches(text: "hello", triggers: triggers))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func trimsAfterTriggerKeepsPostSpeech() {
|
@Test func trimsAfterTriggerKeepsPostSpeech() {
|
||||||
let triggers = ["claude", "clawd"]
|
let triggers = ["claude", "clawd"]
|
||||||
let text = "hey Claude how are you"
|
let text = "hey Claude how are you"
|
||||||
@@ -48,4 +34,46 @@ import Testing
|
|||||||
let text = "claude write a note"
|
let text = "claude write a note"
|
||||||
#expect(VoiceWakeRuntime._testHasContentAfterTrigger(text, triggers: triggers))
|
#expect(VoiceWakeRuntime._testHasContentAfterTrigger(text, triggers: triggers))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func gateRequiresGapBetweenTriggerAndCommand() {
|
||||||
|
let transcript = "hey clawd do thing"
|
||||||
|
let segments = makeSegments(
|
||||||
|
transcript: transcript,
|
||||||
|
words: [
|
||||||
|
("hey", 0.0, 0.1),
|
||||||
|
("clawd", 0.2, 0.1),
|
||||||
|
("do", 0.35, 0.1),
|
||||||
|
("thing", 0.5, 0.1),
|
||||||
|
])
|
||||||
|
let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3)
|
||||||
|
#expect(WakeWordGate.match(transcript: transcript, segments: segments, config: config) == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func gateAcceptsGapAndExtractsCommand() {
|
||||||
|
let transcript = "hey clawd do thing"
|
||||||
|
let segments = makeSegments(
|
||||||
|
transcript: transcript,
|
||||||
|
words: [
|
||||||
|
("hey", 0.0, 0.1),
|
||||||
|
("clawd", 0.2, 0.1),
|
||||||
|
("do", 0.9, 0.1),
|
||||||
|
("thing", 1.1, 0.1),
|
||||||
|
])
|
||||||
|
let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3)
|
||||||
|
#expect(WakeWordGate.match(transcript: transcript, segments: segments, config: config)?.command == "do thing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeSegments(
|
||||||
|
transcript: String,
|
||||||
|
words: [(String, TimeInterval, TimeInterval)])
|
||||||
|
-> [WakeWordSegment] {
|
||||||
|
var searchStart = transcript.startIndex
|
||||||
|
var output: [WakeWordSegment] = []
|
||||||
|
for (word, start, duration) in words {
|
||||||
|
let range = transcript.range(of: word, range: searchStart..<transcript.endIndex)
|
||||||
|
output.append(WakeWordSegment(text: word, start: start, duration: duration, range: range))
|
||||||
|
if let range { searchStart = range.upperBound }
|
||||||
|
}
|
||||||
|
return output
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,47 @@
|
|||||||
|
import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
@testable import Clawdis
|
import SwabbleKit
|
||||||
|
|
||||||
struct VoiceWakeTesterTests {
|
struct VoiceWakeTesterTests {
|
||||||
@Test func matchesIsCaseInsensitiveAndSubstring() {
|
@Test func matchRespectsGapRequirement() {
|
||||||
let triggers = ["Claude", "wake word"]
|
let transcript = "hey claude do thing"
|
||||||
#expect(VoiceWakeTester._testMatches(text: "hey claude are you there", triggers: triggers))
|
let segments = makeSegments(
|
||||||
#expect(VoiceWakeTester._testMatches(text: "this has wake word inside", triggers: triggers))
|
transcript: transcript,
|
||||||
|
words: [
|
||||||
|
("hey", 0.0, 0.1),
|
||||||
|
("claude", 0.2, 0.1),
|
||||||
|
("do", 0.35, 0.1),
|
||||||
|
("thing", 0.5, 0.1),
|
||||||
|
])
|
||||||
|
let config = WakeWordGateConfig(triggers: ["claude"], minPostTriggerGap: 0.3)
|
||||||
|
#expect(WakeWordGate.match(transcript: transcript, segments: segments, config: config) == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func matchesReturnsFalseWhenNoTrigger() {
|
@Test func matchReturnsCommandAfterGap() {
|
||||||
let triggers = ["claude"]
|
let transcript = "hey claude do thing"
|
||||||
#expect(!VoiceWakeTester._testMatches(text: "random text", triggers: triggers))
|
let segments = makeSegments(
|
||||||
|
transcript: transcript,
|
||||||
|
words: [
|
||||||
|
("hey", 0.0, 0.1),
|
||||||
|
("claude", 0.2, 0.1),
|
||||||
|
("do", 0.8, 0.1),
|
||||||
|
("thing", 1.0, 0.1),
|
||||||
|
])
|
||||||
|
let config = WakeWordGateConfig(triggers: ["claude"], minPostTriggerGap: 0.3)
|
||||||
|
#expect(WakeWordGate.match(transcript: transcript, segments: segments, config: config)?.command == "do thing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func makeSegments(
|
||||||
|
transcript: String,
|
||||||
|
words: [(String, TimeInterval, TimeInterval)])
|
||||||
|
-> [WakeWordSegment] {
|
||||||
|
var searchStart = transcript.startIndex
|
||||||
|
var output: [WakeWordSegment] = []
|
||||||
|
for (word, start, duration) in words {
|
||||||
|
let range = transcript.range(of: word, range: searchStart..<transcript.endIndex)
|
||||||
|
output.append(WakeWordSegment(text: word, start: start, duration: duration, range: range))
|
||||||
|
if let range { searchStart = range.upperBound }
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|||||||
1
apps/shared/ClawdisKit/.build/.lock
Normal file
1
apps/shared/ClawdisKit/.build/.lock
Normal file
@@ -0,0 +1 @@
|
|||||||
|
50345
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,311 @@
|
|||||||
|
// Generated by Apple Swift version 6.2.3 (swiftlang-6.2.3.3.21 clang-1700.6.3.2)
|
||||||
|
#ifndef CLAWDISCHATUI_SWIFT_H
|
||||||
|
#define CLAWDISCHATUI_SWIFT_H
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Wgcc-compat"
|
||||||
|
|
||||||
|
#if !defined(__has_include)
|
||||||
|
# define __has_include(x) 0
|
||||||
|
#endif
|
||||||
|
#if !defined(__has_attribute)
|
||||||
|
# define __has_attribute(x) 0
|
||||||
|
#endif
|
||||||
|
#if !defined(__has_feature)
|
||||||
|
# define __has_feature(x) 0
|
||||||
|
#endif
|
||||||
|
#if !defined(__has_warning)
|
||||||
|
# define __has_warning(x) 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<swift/objc-prologue.h>)
|
||||||
|
# include <swift/objc-prologue.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#pragma clang diagnostic ignored "-Wauto-import"
|
||||||
|
#if defined(__OBJC__)
|
||||||
|
#include <Foundation/Foundation.h>
|
||||||
|
#endif
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdbool>
|
||||||
|
#include <cstring>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <new>
|
||||||
|
#include <type_traits>
|
||||||
|
#else
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
#endif
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
|
||||||
|
#if defined(__arm64e__) && __has_include(<ptrauth.h>)
|
||||||
|
# include <ptrauth.h>
|
||||||
|
#else
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
|
||||||
|
# ifndef __ptrauth_swift_value_witness_function_pointer
|
||||||
|
# define __ptrauth_swift_value_witness_function_pointer(x)
|
||||||
|
# endif
|
||||||
|
# ifndef __ptrauth_swift_class_method_pointer
|
||||||
|
# define __ptrauth_swift_class_method_pointer(x)
|
||||||
|
# endif
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
#endif
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(SWIFT_TYPEDEFS)
|
||||||
|
# define SWIFT_TYPEDEFS 1
|
||||||
|
# if __has_include(<uchar.h>)
|
||||||
|
# include <uchar.h>
|
||||||
|
# elif !defined(__cplusplus)
|
||||||
|
typedef unsigned char char8_t;
|
||||||
|
typedef uint_least16_t char16_t;
|
||||||
|
typedef uint_least32_t char32_t;
|
||||||
|
# endif
|
||||||
|
typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
|
||||||
|
typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
|
||||||
|
typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
|
||||||
|
typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
|
||||||
|
typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
|
||||||
|
typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
|
||||||
|
typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
|
||||||
|
typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
|
||||||
|
typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
|
||||||
|
typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
|
||||||
|
typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
|
||||||
|
typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(SWIFT_PASTE)
|
||||||
|
# define SWIFT_PASTE_HELPER(x, y) x##y
|
||||||
|
# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_METATYPE)
|
||||||
|
# define SWIFT_METATYPE(X) Class
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_CLASS_PROPERTY)
|
||||||
|
# if __has_feature(objc_class_property)
|
||||||
|
# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
|
||||||
|
# else
|
||||||
|
# define SWIFT_CLASS_PROPERTY(...)
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_RUNTIME_NAME)
|
||||||
|
# if __has_attribute(objc_runtime_name)
|
||||||
|
# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
|
||||||
|
# else
|
||||||
|
# define SWIFT_RUNTIME_NAME(X)
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_COMPILE_NAME)
|
||||||
|
# if __has_attribute(swift_name)
|
||||||
|
# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
|
||||||
|
# else
|
||||||
|
# define SWIFT_COMPILE_NAME(X)
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_METHOD_FAMILY)
|
||||||
|
# if __has_attribute(objc_method_family)
|
||||||
|
# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
|
||||||
|
# else
|
||||||
|
# define SWIFT_METHOD_FAMILY(X)
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_NOESCAPE)
|
||||||
|
# if __has_attribute(noescape)
|
||||||
|
# define SWIFT_NOESCAPE __attribute__((noescape))
|
||||||
|
# else
|
||||||
|
# define SWIFT_NOESCAPE
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_RELEASES_ARGUMENT)
|
||||||
|
# if __has_attribute(ns_consumed)
|
||||||
|
# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
|
||||||
|
# else
|
||||||
|
# define SWIFT_RELEASES_ARGUMENT
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_WARN_UNUSED_RESULT)
|
||||||
|
# if __has_attribute(warn_unused_result)
|
||||||
|
# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
|
||||||
|
# else
|
||||||
|
# define SWIFT_WARN_UNUSED_RESULT
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_NORETURN)
|
||||||
|
# if __has_attribute(noreturn)
|
||||||
|
# define SWIFT_NORETURN __attribute__((noreturn))
|
||||||
|
# else
|
||||||
|
# define SWIFT_NORETURN
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_CLASS_EXTRA)
|
||||||
|
# define SWIFT_CLASS_EXTRA
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_PROTOCOL_EXTRA)
|
||||||
|
# define SWIFT_PROTOCOL_EXTRA
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_ENUM_EXTRA)
|
||||||
|
# define SWIFT_ENUM_EXTRA
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_CLASS)
|
||||||
|
# if __has_attribute(objc_subclassing_restricted)
|
||||||
|
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
|
||||||
|
# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
|
||||||
|
# else
|
||||||
|
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
|
||||||
|
# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_RESILIENT_CLASS)
|
||||||
|
# if __has_attribute(objc_class_stub)
|
||||||
|
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
|
||||||
|
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
|
||||||
|
# else
|
||||||
|
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
|
||||||
|
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_PROTOCOL)
|
||||||
|
# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
|
||||||
|
# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_EXTENSION)
|
||||||
|
# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
|
||||||
|
#endif
|
||||||
|
#if !defined(OBJC_DESIGNATED_INITIALIZER)
|
||||||
|
# if __has_attribute(objc_designated_initializer)
|
||||||
|
# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
|
||||||
|
# else
|
||||||
|
# define OBJC_DESIGNATED_INITIALIZER
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_ENUM_ATTR)
|
||||||
|
# if __has_attribute(enum_extensibility)
|
||||||
|
# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
|
||||||
|
# else
|
||||||
|
# define SWIFT_ENUM_ATTR(_extensibility)
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_ENUM)
|
||||||
|
# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
|
||||||
|
# if __has_feature(generalized_swift_name)
|
||||||
|
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
|
||||||
|
# else
|
||||||
|
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_UNAVAILABLE)
|
||||||
|
# define SWIFT_UNAVAILABLE __attribute__((unavailable))
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_UNAVAILABLE_MSG)
|
||||||
|
# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_AVAILABILITY)
|
||||||
|
# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_WEAK_IMPORT)
|
||||||
|
# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_DEPRECATED)
|
||||||
|
# define SWIFT_DEPRECATED __attribute__((deprecated))
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_DEPRECATED_MSG)
|
||||||
|
# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_DEPRECATED_OBJC)
|
||||||
|
# if __has_feature(attribute_diagnose_if_objc)
|
||||||
|
# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
|
||||||
|
# else
|
||||||
|
# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if defined(__OBJC__)
|
||||||
|
#if !defined(IBSegueAction)
|
||||||
|
# define IBSegueAction
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_EXTERN)
|
||||||
|
# if defined(__cplusplus)
|
||||||
|
# define SWIFT_EXTERN extern "C"
|
||||||
|
# else
|
||||||
|
# define SWIFT_EXTERN extern
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_CALL)
|
||||||
|
# define SWIFT_CALL __attribute__((swiftcall))
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_INDIRECT_RESULT)
|
||||||
|
# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_CONTEXT)
|
||||||
|
# define SWIFT_CONTEXT __attribute__((swift_context))
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_ERROR_RESULT)
|
||||||
|
# define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
|
||||||
|
#endif
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
# define SWIFT_NOEXCEPT noexcept
|
||||||
|
#else
|
||||||
|
# define SWIFT_NOEXCEPT
|
||||||
|
#endif
|
||||||
|
#if !defined(SWIFT_C_INLINE_THUNK)
|
||||||
|
# if __has_attribute(always_inline)
|
||||||
|
# if __has_attribute(nodebug)
|
||||||
|
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug))
|
||||||
|
# else
|
||||||
|
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline))
|
||||||
|
# endif
|
||||||
|
# else
|
||||||
|
# define SWIFT_C_INLINE_THUNK inline
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
|
||||||
|
# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport)
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
|
||||||
|
# define SWIFT_IMPORT_STDLIB_SYMBOL
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#if defined(__OBJC__)
|
||||||
|
#if __has_feature(objc_modules)
|
||||||
|
#if __has_warning("-Watimport-in-framework-header")
|
||||||
|
#pragma clang diagnostic ignored "-Watimport-in-framework-header"
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
|
||||||
|
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
|
||||||
|
#if __has_warning("-Wpragma-clang-attribute")
|
||||||
|
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
|
||||||
|
#endif
|
||||||
|
#pragma clang diagnostic ignored "-Wunknown-pragmas"
|
||||||
|
#pragma clang diagnostic ignored "-Wnullability"
|
||||||
|
#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
|
||||||
|
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
|
||||||
|
|
||||||
|
#if __has_attribute(external_source_symbol)
|
||||||
|
# pragma push_macro("any")
|
||||||
|
# undef any
|
||||||
|
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="ClawdisChatUI",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
|
||||||
|
# pragma pop_macro("any")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(__OBJC__)
|
||||||
|
|
||||||
|
#endif
|
||||||
|
#if __has_attribute(external_source_symbol)
|
||||||
|
# pragma clang attribute pop
|
||||||
|
#endif
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
#endif
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module ClawdisChatUI {
|
||||||
|
header "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/include/ClawdisChatUI-Swift.h"
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"": {
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/master.swiftdeps"
|
||||||
|
},
|
||||||
|
"/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatComposer.swift": {
|
||||||
|
"dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatComposer.d",
|
||||||
|
"object": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatComposer.swift.o",
|
||||||
|
"swiftmodule": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatComposer~partial.swiftmodule",
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatComposer.swiftdeps",
|
||||||
|
"diagnostics": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatComposer.dia"
|
||||||
|
},
|
||||||
|
"/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatMarkdownSplitter.swift": {
|
||||||
|
"dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatMarkdownSplitter.d",
|
||||||
|
"object": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatMarkdownSplitter.swift.o",
|
||||||
|
"swiftmodule": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatMarkdownSplitter~partial.swiftmodule",
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatMarkdownSplitter.swiftdeps",
|
||||||
|
"diagnostics": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatMarkdownSplitter.dia"
|
||||||
|
},
|
||||||
|
"/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatMessageViews.swift": {
|
||||||
|
"dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatMessageViews.d",
|
||||||
|
"object": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatMessageViews.swift.o",
|
||||||
|
"swiftmodule": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatMessageViews~partial.swiftmodule",
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatMessageViews.swiftdeps",
|
||||||
|
"diagnostics": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatMessageViews.dia"
|
||||||
|
},
|
||||||
|
"/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatModels.swift": {
|
||||||
|
"dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatModels.d",
|
||||||
|
"object": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatModels.swift.o",
|
||||||
|
"swiftmodule": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatModels~partial.swiftmodule",
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatModels.swiftdeps",
|
||||||
|
"diagnostics": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatModels.dia"
|
||||||
|
},
|
||||||
|
"/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatPayloadDecoding.swift": {
|
||||||
|
"dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatPayloadDecoding.d",
|
||||||
|
"object": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatPayloadDecoding.swift.o",
|
||||||
|
"swiftmodule": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatPayloadDecoding~partial.swiftmodule",
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatPayloadDecoding.swiftdeps",
|
||||||
|
"diagnostics": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatPayloadDecoding.dia"
|
||||||
|
},
|
||||||
|
"/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatSessions.swift": {
|
||||||
|
"dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatSessions.d",
|
||||||
|
"object": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatSessions.swift.o",
|
||||||
|
"swiftmodule": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatSessions~partial.swiftmodule",
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatSessions.swiftdeps",
|
||||||
|
"diagnostics": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatSessions.dia"
|
||||||
|
},
|
||||||
|
"/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatSheets.swift": {
|
||||||
|
"dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatSheets.d",
|
||||||
|
"object": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatSheets.swift.o",
|
||||||
|
"swiftmodule": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatSheets~partial.swiftmodule",
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatSheets.swiftdeps",
|
||||||
|
"diagnostics": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatSheets.dia"
|
||||||
|
},
|
||||||
|
"/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatTheme.swift": {
|
||||||
|
"dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatTheme.d",
|
||||||
|
"object": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatTheme.swift.o",
|
||||||
|
"swiftmodule": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatTheme~partial.swiftmodule",
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatTheme.swiftdeps",
|
||||||
|
"diagnostics": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatTheme.dia"
|
||||||
|
},
|
||||||
|
"/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatTransport.swift": {
|
||||||
|
"dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatTransport.d",
|
||||||
|
"object": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatTransport.swift.o",
|
||||||
|
"swiftmodule": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatTransport~partial.swiftmodule",
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatTransport.swiftdeps",
|
||||||
|
"diagnostics": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatTransport.dia"
|
||||||
|
},
|
||||||
|
"/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift": {
|
||||||
|
"dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatView.d",
|
||||||
|
"object": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatView.swift.o",
|
||||||
|
"swiftmodule": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatView~partial.swiftmodule",
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatView.swiftdeps",
|
||||||
|
"diagnostics": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatView.dia"
|
||||||
|
},
|
||||||
|
"/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatViewModel.swift": {
|
||||||
|
"dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatViewModel.d",
|
||||||
|
"object": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatViewModel.swift.o",
|
||||||
|
"swiftmodule": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatViewModel~partial.swiftmodule",
|
||||||
|
"swift-dependencies": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatViewModel.swiftdeps",
|
||||||
|
"diagnostics": "/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/.build/arm64-apple-macosx/debug/ClawdisChatUI.build/ChatViewModel.dia"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatComposer.swift
|
||||||
|
/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatMarkdownSplitter.swift
|
||||||
|
/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatMessageViews.swift
|
||||||
|
/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatModels.swift
|
||||||
|
/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatPayloadDecoding.swift
|
||||||
|
/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatSessions.swift
|
||||||
|
/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatSheets.swift
|
||||||
|
/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatTheme.swift
|
||||||
|
/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatTransport.swift
|
||||||
|
/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift
|
||||||
|
/Users/steipete/Projects/clawdis/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatViewModel.swift
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user