Files
ScarriffleandClaude Opus 5 a3399d3a96 Zwei Anzeigen für einen Handgriff sind eine zu viel
Apples eigene Einblendung lässt sich aushängen, ohne Root und ohne SIP
anzufassen: `com.apple.OSDUIHelper` liegt in der GUI-Domäne des angemeldeten
Benutzers, nicht im System. `launchctl bootout` dort hinein genügt.

Standardmäßig aus, denn es verschwindet **jede** Einblendung — auch Helligkeit
und Feststelltaste, und für die Feststelltaste hat Onyx keinen Ersatz. Beim
Beenden wird wieder eingehängt: eine Einstellung darf nicht überleben, was sie
eingestellt hat.

Dazu zwei Fehler im Anlernen behoben. Der Posenname stand als roher Schlüssel da
— die Zeichenketten liegen im Bündel von `FaceProvider`, gesucht wurden sie im
Hauptbündel. Und „Gesicht vergleichen" blieb nach dem Anlernen grau, weil
`isEnrolled` bei jedem Aufruf den Schlüsselbund las: richtig, aber für SwiftUI
unsichtbar, weil es nichts gab, woran eine Ansicht die Änderung hätte bemerken
können.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 22:26:41 +02:00

118 lines
3.8 KiB
Swift

import SwiftUI
import AVFoundation
import Observation
import OnyxDesign
/// Führt durch das Anlernen: mehrere Posen, mehrere Aufnahmen je Pose.
///
/// **Warum mehrere Posen.** Der Merkmalsvektor ist ein allgemeiner
/// Bildmerkmalsvektor und kein Gesichtsmerkmal — er reagiert auf Kopfhaltung
/// und Beleuchtung mit. Eine einzige Aufnahme beschreibt genau eine Haltung bei
/// genau einem Licht; wer sich danach anders hinsetzt, wird nicht erkannt.
@MainActor
@Observable
public final class FaceEnrollment {
public enum Pose: String, CaseIterable, Sendable {
case straight, left, right
/// Aufgelöst **hier** und nicht als Schlüssel weitergereicht: die
/// Zeichenketten liegen im Bündel dieses Moduls, und wer sie anderswo
/// nachschlägt, bekommt den Schlüssel selbst zu sehen.
public var localizedName: String {
switch self {
case .straight: String(localized: "face.pose.straight", bundle: .module)
case .left: String(localized: "face.pose.left", bundle: .module)
case .right: String(localized: "face.pose.right", bundle: .module)
}
}
}
/// Wie viele Aufnahmen je Pose.
public static let samplesPerPose = 5
public private(set) var pose: Pose = .straight
public private(set) var collected = 0
public private(set) var isFinished = false
/// Ob gerade ein Gesicht im Bild ist — für die Rückmeldung „bitte ins Bild".
public private(set) var seesFace = false
@ObservationIgnored private let capture = FaceCapture()
@ObservationIgnored private let store = FaceStore()
@ObservationIgnored private var templates: [FaceTemplate] = []
/// Erst auf Knopfdruck aufnehmen: sonst sammelt es fünf Bilder derselben
/// Sekunde ein, und das ist keine Vielfalt, sondern dasselbe fünfmal.
@ObservationIgnored private var isCollecting = false
public var failure: String? { capture.failure }
/// Für die Vorschau.
public var session: AVCaptureSession { capture.session }
public init() {}
public func start() {
capture.onVector = { [weak self] vector in self?.accept(vector) }
capture.onNoFace = { [weak self] in self?.seesFace = false }
capture.start()
}
public func stop() { capture.stop() }
/// Nimmt die aktuelle Pose auf.
public func captureCurrentPose() { isCollecting = true }
private func accept(_ vector: [Float]) {
seesFace = true
guard isCollecting, !isFinished else { return }
templates.append(FaceTemplate(pose: pose.rawValue, vector: vector))
collected += 1
guard collected >= Self.samplesPerPose else { return }
isCollecting = false
collected = 0
guard let next = Pose.allCases.firstIndex(of: pose).map({ $0 + 1 }),
next < Pose.allCases.count else {
finish()
return
}
pose = Pose.allCases[next]
}
private func finish() {
capture.stop()
store.save(templates)
isFinished = true
}
/// Verwirft alles Angelernte.
public func reset() {
capture.stop()
templates = []
collected = 0
pose = .straight
isFinished = false
store.clear()
}
}
/// Die Kameravorschau.
public struct CameraPreview: NSViewRepresentable {
private let session: AVCaptureSession
public init(session: AVCaptureSession) { self.session = session }
public func makeNSView(context: Context) -> NSView {
let view = NSView()
view.wantsLayer = true
let layer = AVCaptureVideoPreviewLayer(session: session)
layer.videoGravity = .resizeAspectFill
view.layer = layer
return view
}
public func updateNSView(_ view: NSView, context: Context) {}
}