lol
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// AdaptiveLayout.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by Claude on 1/19/26.
|
||||
// Created by Mike Freno on 1/19/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
@@ -13,16 +13,16 @@ enum AdaptiveLayout {
|
||||
enum Window {
|
||||
static let minWidth: CGFloat = 700
|
||||
#if APPSTORE
|
||||
static let minHeight: CGFloat = 500
|
||||
static let minHeight: CGFloat = 500
|
||||
#else
|
||||
static let minHeight: CGFloat = 600
|
||||
static let minHeight: CGFloat = 600
|
||||
#endif
|
||||
|
||||
static let defaultWidth: CGFloat = 900
|
||||
#if APPSTORE
|
||||
static let defaultHeight: CGFloat = 650
|
||||
static let defaultHeight: CGFloat = 650
|
||||
#else
|
||||
static let defaultHeight: CGFloat = 800
|
||||
static let defaultHeight: CGFloat = 800
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// TestingEnvironment.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by OpenCode on 1/13/26.
|
||||
// Created by Mike Freno on 1/13/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -26,13 +26,14 @@ enum TestingEnvironment {
|
||||
|
||||
/// Check if running in any test mode (unit tests or UI tests)
|
||||
static var isAnyTestMode: Bool {
|
||||
return isUITesting || ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
|
||||
return isUITesting
|
||||
|| ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
/// Check if dev triggers should be visible
|
||||
static var shouldShowDevTriggers: Bool {
|
||||
return isUITesting || isAnyTestMode
|
||||
}
|
||||
/// Check if dev triggers should be visible
|
||||
static var shouldShowDevTriggers: Bool {
|
||||
return isUITesting || isAnyTestMode
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// ScreenCapturePermissionManager.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by ChatGPT on 1/14/26.
|
||||
// Created by Mike Freno on 1/14/26.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// GazeOverlayView.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by Claude on 1/16/26.
|
||||
// Created by Mike Freno on 1/16/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// PupilOverlayView.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by Claude on 1/16/26.
|
||||
// Created by Mike Freno on 1/16/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// UserTimerOverlayReminderView.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by OpenCode on 1/11/26.
|
||||
// Created by Mike Freno on 1/11/26.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// UserTimerReminderView.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by OpenCode on 1/11/26.
|
||||
// Created by Mike Freno on 1/11/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
// PupilDetectorTests.swift
|
||||
// GazeTests
|
||||
//
|
||||
// Created by Claude on 1/16/26.
|
||||
// Created by Mike Freno on 1/16/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import CoreVideo
|
||||
import Vision
|
||||
import XCTest
|
||||
|
||||
@testable import Gaze
|
||||
|
||||
final class PupilDetectorTests: XCTestCase {
|
||||
@@ -103,7 +104,9 @@ final class PupilDetectorTests: XCTestCase {
|
||||
shouldInterpolate: false,
|
||||
intent: .defaultIntent
|
||||
) {
|
||||
if let dest = CGImageDestinationCreateWithURL(url as CFURL, "public.png" as CFString, 1, nil) {
|
||||
if let dest = CGImageDestinationCreateWithURL(
|
||||
url as CFURL, "public.png" as CFString, 1, nil)
|
||||
{
|
||||
CGImageDestinationAddImage(dest, cgImage, nil)
|
||||
CGImageDestinationFinalize(dest)
|
||||
print("💾 Saved synthetic test input to: \(url.path)")
|
||||
@@ -159,7 +162,9 @@ final class PupilDetectorTests: XCTestCase {
|
||||
shouldInterpolate: false,
|
||||
intent: .defaultIntent
|
||||
) {
|
||||
if let dest = CGImageDestinationCreateWithURL(url as CFURL, "public.png" as CFString, 1, nil) {
|
||||
if let dest = CGImageDestinationCreateWithURL(
|
||||
url as CFURL, "public.png" as CFString, 1, nil)
|
||||
{
|
||||
CGImageDestinationAddImage(dest, cgImage, nil)
|
||||
CGImageDestinationFinalize(dest)
|
||||
print("💾 Saved synthetic binary image to: \(url.path)")
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
// VideoGazeTests.swift
|
||||
// GazeTests
|
||||
//
|
||||
// Created by Claude on 1/16/26.
|
||||
// Created by Mike Freno on 1/16/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import AVFoundation
|
||||
import Vision
|
||||
import XCTest
|
||||
|
||||
@testable import Gaze
|
||||
|
||||
final class VideoGazeTests: XCTestCase {
|
||||
@@ -34,19 +35,32 @@ final class VideoGazeTests: XCTestCase {
|
||||
XCTFail("Video file not found at: \(projectPath)")
|
||||
return
|
||||
}
|
||||
let stats = try await processVideo(at: URL(fileURLWithPath: projectPath), expectLookingAway: true)
|
||||
let stats = try await processVideo(
|
||||
at: URL(fileURLWithPath: projectPath), expectLookingAway: true)
|
||||
|
||||
// For outer video, most frames should detect gaze outside center
|
||||
let nonCenterRatio = Double(stats.nonCenterFrames) / Double(max(1, stats.pupilDetectedFrames))
|
||||
log("🎯 OUTER video: \(String(format: "%.1f%%", nonCenterRatio * 100)) frames detected as non-center (expected: >50%)")
|
||||
log(" H-range: \(String(format: "%.3f", stats.minH)) to \(String(format: "%.3f", stats.maxH))")
|
||||
log(" V-range: \(String(format: "%.3f", stats.minV)) to \(String(format: "%.3f", stats.maxV))")
|
||||
log(" Face width: \(String(format: "%.3f", stats.avgFaceWidth)) (range: \(String(format: "%.3f", stats.minFaceWidth))-\(String(format: "%.3f", stats.maxFaceWidth)))")
|
||||
let nonCenterRatio =
|
||||
Double(stats.nonCenterFrames) / Double(max(1, stats.pupilDetectedFrames))
|
||||
log(
|
||||
"🎯 OUTER video: \(String(format: "%.1f%%", nonCenterRatio * 100)) frames detected as non-center (expected: >50%)"
|
||||
)
|
||||
log(
|
||||
" H-range: \(String(format: "%.3f", stats.minH)) to \(String(format: "%.3f", stats.maxH))"
|
||||
)
|
||||
log(
|
||||
" V-range: \(String(format: "%.3f", stats.minV)) to \(String(format: "%.3f", stats.maxV))"
|
||||
)
|
||||
log(
|
||||
" Face width: \(String(format: "%.3f", stats.avgFaceWidth)) (range: \(String(format: "%.3f", stats.minFaceWidth))-\(String(format: "%.3f", stats.maxFaceWidth)))"
|
||||
)
|
||||
|
||||
attachLogs()
|
||||
|
||||
// At least 50% should be detected as non-center when looking away
|
||||
XCTAssertGreaterThan(nonCenterRatio, 0.5, "Looking away video should have >50% non-center detections. Log:\n\(logLines.joined(separator: "\n"))")
|
||||
XCTAssertGreaterThan(
|
||||
nonCenterRatio, 0.5,
|
||||
"Looking away video should have >50% non-center detections. Log:\n\(logLines.joined(separator: "\n"))"
|
||||
)
|
||||
}
|
||||
|
||||
/// Process the inner video (looking at screen) - should detect "looking at screen"
|
||||
@@ -58,19 +72,31 @@ final class VideoGazeTests: XCTestCase {
|
||||
XCTFail("Video file not found at: \(projectPath)")
|
||||
return
|
||||
}
|
||||
let stats = try await processVideo(at: URL(fileURLWithPath: projectPath), expectLookingAway: false)
|
||||
let stats = try await processVideo(
|
||||
at: URL(fileURLWithPath: projectPath), expectLookingAway: false)
|
||||
|
||||
// For inner video, most frames should detect gaze at center
|
||||
let centerRatio = Double(stats.centerFrames) / Double(max(1, stats.pupilDetectedFrames))
|
||||
log("🎯 INNER video: \(String(format: "%.1f%%", centerRatio * 100)) frames detected as center (expected: >50%)")
|
||||
log(" H-range: \(String(format: "%.3f", stats.minH)) to \(String(format: "%.3f", stats.maxH))")
|
||||
log(" V-range: \(String(format: "%.3f", stats.minV)) to \(String(format: "%.3f", stats.maxV))")
|
||||
log(" Face width: \(String(format: "%.3f", stats.avgFaceWidth)) (range: \(String(format: "%.3f", stats.minFaceWidth))-\(String(format: "%.3f", stats.maxFaceWidth)))")
|
||||
log(
|
||||
"🎯 INNER video: \(String(format: "%.1f%%", centerRatio * 100)) frames detected as center (expected: >50%)"
|
||||
)
|
||||
log(
|
||||
" H-range: \(String(format: "%.3f", stats.minH)) to \(String(format: "%.3f", stats.maxH))"
|
||||
)
|
||||
log(
|
||||
" V-range: \(String(format: "%.3f", stats.minV)) to \(String(format: "%.3f", stats.maxV))"
|
||||
)
|
||||
log(
|
||||
" Face width: \(String(format: "%.3f", stats.avgFaceWidth)) (range: \(String(format: "%.3f", stats.minFaceWidth))-\(String(format: "%.3f", stats.maxFaceWidth)))"
|
||||
)
|
||||
|
||||
attachLogs()
|
||||
|
||||
// At least 50% should be detected as center when looking at screen
|
||||
XCTAssertGreaterThan(centerRatio, 0.5, "Looking at screen video should have >50% center detections. Log:\n\(logLines.joined(separator: "\n"))")
|
||||
XCTAssertGreaterThan(
|
||||
centerRatio, 0.5,
|
||||
"Looking at screen video should have >50% center detections. Log:\n\(logLines.joined(separator: "\n"))"
|
||||
)
|
||||
}
|
||||
|
||||
struct VideoStats {
|
||||
@@ -98,7 +124,9 @@ final class VideoGazeTests: XCTestCase {
|
||||
|
||||
log("\n" + String(repeating: "=", count: 60))
|
||||
log("Processing video: \(url.lastPathComponent)")
|
||||
log("Expected behavior: \(expectLookingAway ? "LOOKING AWAY (non-center)" : "LOOKING AT SCREEN (center)")")
|
||||
log(
|
||||
"Expected behavior: \(expectLookingAway ? "LOOKING AWAY (non-center)" : "LOOKING AT SCREEN (center)")"
|
||||
)
|
||||
log(String(repeating: "=", count: 60))
|
||||
|
||||
let asset = AVURLAsset(url: url)
|
||||
@@ -113,7 +141,9 @@ final class VideoGazeTests: XCTestCase {
|
||||
|
||||
let size = try await track.load(.naturalSize)
|
||||
let frameRate = try await track.load(.nominalFrameRate)
|
||||
log("Size: \(Int(size.width))x\(Int(size.height)), FPS: \(String(format: "%.1f", frameRate))")
|
||||
log(
|
||||
"Size: \(Int(size.width))x\(Int(size.height)), FPS: \(String(format: "%.1f", frameRate))"
|
||||
)
|
||||
|
||||
let reader = try AVAssetReader(asset: asset)
|
||||
let outputSettings: [String: Any] = [
|
||||
@@ -124,7 +154,7 @@ final class VideoGazeTests: XCTestCase {
|
||||
reader.startReading()
|
||||
|
||||
var frameIndex = 0
|
||||
let sampleInterval = max(1, Int(frameRate / 2)) // Sample ~2 frames per second
|
||||
let sampleInterval = max(1, Int(frameRate / 2)) // Sample ~2 frames per second
|
||||
|
||||
log("\nFrame | Time | Face | H-Ratio L/R | V-Ratio L/R | Direction")
|
||||
log(String(repeating: "-", count: 75))
|
||||
@@ -170,11 +200,15 @@ final class VideoGazeTests: XCTestCase {
|
||||
try handler.perform([request])
|
||||
|
||||
guard let observations = request.results, !observations.isEmpty,
|
||||
let face = observations.first,
|
||||
let landmarks = face.landmarks,
|
||||
let leftEye = landmarks.leftEye,
|
||||
let rightEye = landmarks.rightEye else {
|
||||
log(String(format: "%5d | %5.1fs | NO | - | - | -", frameIndex, timeSeconds))
|
||||
let face = observations.first,
|
||||
let landmarks = face.landmarks,
|
||||
let leftEye = landmarks.leftEye,
|
||||
let rightEye = landmarks.rightEye
|
||||
else {
|
||||
log(
|
||||
String(
|
||||
format: "%5d | %5.1fs | NO | - | - | -",
|
||||
frameIndex, timeSeconds))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -205,8 +239,10 @@ final class VideoGazeTests: XCTestCase {
|
||||
imageSize: imageSize,
|
||||
side: 0
|
||||
) {
|
||||
leftHRatio = calculateHorizontalRatio(pupilPosition: leftResult.pupilPosition, eyeRegion: leftResult.eyeRegion)
|
||||
leftVRatio = calculateVerticalRatio(pupilPosition: leftResult.pupilPosition, eyeRegion: leftResult.eyeRegion)
|
||||
leftHRatio = calculateHorizontalRatio(
|
||||
pupilPosition: leftResult.pupilPosition, eyeRegion: leftResult.eyeRegion)
|
||||
leftVRatio = calculateVerticalRatio(
|
||||
pupilPosition: leftResult.pupilPosition, eyeRegion: leftResult.eyeRegion)
|
||||
}
|
||||
|
||||
if let rightResult = PupilDetector.detectPupil(
|
||||
@@ -216,12 +252,15 @@ final class VideoGazeTests: XCTestCase {
|
||||
imageSize: imageSize,
|
||||
side: 1
|
||||
) {
|
||||
rightHRatio = calculateHorizontalRatio(pupilPosition: rightResult.pupilPosition, eyeRegion: rightResult.eyeRegion)
|
||||
rightVRatio = calculateVerticalRatio(pupilPosition: rightResult.pupilPosition, eyeRegion: rightResult.eyeRegion)
|
||||
rightHRatio = calculateHorizontalRatio(
|
||||
pupilPosition: rightResult.pupilPosition, eyeRegion: rightResult.eyeRegion)
|
||||
rightVRatio = calculateVerticalRatio(
|
||||
pupilPosition: rightResult.pupilPosition, eyeRegion: rightResult.eyeRegion)
|
||||
}
|
||||
|
||||
if let lh = leftHRatio, let rh = rightHRatio,
|
||||
let lv = leftVRatio, let rv = rightVRatio {
|
||||
let lv = leftVRatio, let rv = rightVRatio
|
||||
{
|
||||
stats.pupilDetectedFrames += 1
|
||||
let avgH = (lh + rh) / 2.0
|
||||
let avgV = (lv + rv) / 2.0
|
||||
@@ -238,23 +277,35 @@ final class VideoGazeTests: XCTestCase {
|
||||
} else {
|
||||
stats.nonCenterFrames += 1
|
||||
}
|
||||
log(String(format: "%5d | %5.1fs | YES | %.2f / %.2f | %.2f / %.2f | %@ %@",
|
||||
frameIndex, timeSeconds, lh, rh, lv, rv, direction.rawValue, String(describing: direction)))
|
||||
log(
|
||||
String(
|
||||
format: "%5d | %5.1fs | YES | %.2f / %.2f | %.2f / %.2f | %@ %@",
|
||||
frameIndex, timeSeconds, lh, rh, lv, rv, direction.rawValue,
|
||||
String(describing: direction)))
|
||||
} else {
|
||||
log(String(format: "%5d | %5.1fs | YES | PUPIL FAIL | PUPIL FAIL | -", frameIndex, timeSeconds))
|
||||
log(
|
||||
String(
|
||||
format: "%5d | %5.1fs | YES | PUPIL FAIL | PUPIL FAIL | -",
|
||||
frameIndex, timeSeconds))
|
||||
}
|
||||
}
|
||||
|
||||
log(String(repeating: "=", count: 75))
|
||||
log("Summary: \(stats.totalFrames) frames sampled, \(stats.faceDetectedFrames) with face, \(stats.pupilDetectedFrames) with pupils")
|
||||
log(
|
||||
"Summary: \(stats.totalFrames) frames sampled, \(stats.faceDetectedFrames) with face, \(stats.pupilDetectedFrames) with pupils"
|
||||
)
|
||||
log("Center frames: \(stats.centerFrames), Non-center: \(stats.nonCenterFrames)")
|
||||
log("Face width: avg=\(String(format: "%.3f", stats.avgFaceWidth)), range=\(String(format: "%.3f", stats.minFaceWidth)) to \(String(format: "%.3f", stats.maxFaceWidth))")
|
||||
log(
|
||||
"Face width: avg=\(String(format: "%.3f", stats.avgFaceWidth)), range=\(String(format: "%.3f", stats.minFaceWidth)) to \(String(format: "%.3f", stats.maxFaceWidth))"
|
||||
)
|
||||
log("Processing complete\n")
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
private func calculateHorizontalRatio(pupilPosition: PupilPosition, eyeRegion: EyeRegion) -> Double {
|
||||
private func calculateHorizontalRatio(pupilPosition: PupilPosition, eyeRegion: EyeRegion)
|
||||
-> Double
|
||||
{
|
||||
// pupilPosition.y controls horizontal gaze due to image orientation
|
||||
let pupilY = Double(pupilPosition.y)
|
||||
let eyeHeight = Double(eyeRegion.frame.height)
|
||||
@@ -265,7 +316,9 @@ final class VideoGazeTests: XCTestCase {
|
||||
return max(0.0, min(1.0, ratio))
|
||||
}
|
||||
|
||||
private func calculateVerticalRatio(pupilPosition: PupilPosition, eyeRegion: EyeRegion) -> Double {
|
||||
private func calculateVerticalRatio(pupilPosition: PupilPosition, eyeRegion: EyeRegion)
|
||||
-> Double
|
||||
{
|
||||
// pupilPosition.x controls vertical gaze due to image orientation
|
||||
let pupilX = Double(pupilPosition.x)
|
||||
let eyeWidth = Double(eyeRegion.frame.width)
|
||||
|
||||
Reference in New Issue
Block a user