Skip to content

feat: FreeWispr v1.3 — quality, resilience, and accessibility#4

Merged
ygivenx merged 15 commits into
mainfrom
feat/v1.3-quality
Mar 23, 2026
Merged

feat: FreeWispr v1.3 — quality, resilience, and accessibility#4
ygivenx merged 15 commits into
mainfrom
feat/v1.3-quality

Conversation

@ygivenx
Copy link
Copy Markdown
Owner

@ygivenx ygivenx commented Mar 23, 2026

Summary

  • Add on-device LLM text correction via Apple Intelligence with app-aware context and 5s timeout
  • Add 30s Whisper inference timeout with clean cancellation
  • Add audio validation (min duration, silence rejection) and peak normalization
  • Add floating recording indicator dot with pulse animation (respects Reduce Motion)
  • Add color-coded status dots and VoiceOver announcements for errors/warnings
  • Add GGML model validation after download with auto-delete on corruption
  • Add DMG update signature verification via SecStaticCode
  • Add audio hardware config change detection with automatic engine rebuild
  • Fix data race in audio tap callback with thread-safe recording flag
  • Fix model switching to download before unloading previous model
  • Improve BundleExtension with safer fallback chain
  • Improve build script with resource bundle check, DMG notarization, and Gatekeeper verification

Test Coverage

47 tests pass (1 benchmark skipped). 28/28 pure logic paths covered including:

  • TextCorrector: refusal detection, app context building, category mappings (13 tests)
  • ModelValidation: GGML magic bytes, truncated/empty/missing files (6 tests)
  • UpdateChecker: semver comparison across all edge cases (9 tests)
  • ModelManager: URL generation, local paths, Core ML paths (5 tests)
  • AudioRecorder: initial state verification (1 test)
  • WhisperTranscriber: initial state, silence handling (2 tests)
  • TextInjector: clipboard preservation and rich content (3 tests)
  • ModelSize/ModelDownloadError: enum coverage (8 tests)

Pre-Landing Review

No issues found.

Adversarial Review

Claude adversarial subagent: 13 informational findings, no ship-blockers. Key items:

  • GGML magic bytes confirmed correct for SwiftWhisper ggml-format models
  • AudioRecorder needsRebuild has theoretical data race (benign — worst case is missed rebuild)
  • Gain normalization has no cap (enhancement, not bug — very quiet audio still transcribes)
  • RecordingIndicator missing @MainActor annotation (called from MainActor context, safe in practice)

TODOS

No TODO items completed in this PR. 1 item remaining (P2: Test Audio Corpus for WER Measurement).

Test plan

  • All Swift tests pass (47 tests, 0 failures)
  • Pre-landing review clean
  • Adversarial review — informational only

🤖 Generated with Claude Code

ygivenx and others added 12 commits March 22, 2026 13:34
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents the app from hanging indefinitely on long or malformed audio.
Uses SwiftWhisper's native cancel() via abort callback instead of
force-killing the task. Maps CancellationError and WhisperError.cancelled
to a new TranscriberError.timeout for clean error handling upstream.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ntext

- 5s timeout racing against LLM correction so dictation always completes
- Refusal detection catches model commentary instead of corrections
- App-type categories (code editor, browser, messaging) for context-aware
  correction — preserves camelCase/snake_case in code editors
- Thread-safe: appName and bundleID captured on @mainactor before async

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace isRecording race with _isCapturing guarded by bufferQueue
- Observe AVAudioEngine.configurationChangeNotification for hardware
  changes (BT headset connect, mic switch, app reconfig)
- needsRebuild flag defers tap rebuild to next startRecording() call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Checks magic bytes (0x67676d6c) and file size after download.
Deletes corrupted files so the next attempt re-downloads cleanly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extracts Team ID from the running app's own signature at init.
Mounts downloaded DMG, finds the .app, verifies signature matches
expected Team ID via SecStaticCodeCheckValidityWithErrors.
Falls back to release page on verification failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- NSPanel with 12pt pulsing red dot at screen top-center while recording
- Respects Reduce Motion accessibility setting
- Status dot: red (recording/error), orange (warning), blue (processing),
  green (ready)
- VoiceOver announcements for error and warning states

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Reject recordings < 0.3s with "Didn't catch that — hold longer"
- Reject silence (peak < 0.01) with "Too quiet — speak louder"
- Normalize quiet audio to 0.7 peak amplitude for consistent inference
- showTemporaryStatus helper auto-reverts to "Ready" after 2s
- Error/timeout status messages now auto-revert
- Structured logging via os.log for transcription failures
- switchModel defers unload until after successful download
- Recording indicator wired into start/stop lifecycle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- BundleExtension checks Contents/Resources/ for signed .app bundles
- Build script improvements for notarization workflow
- Add TODOS.md for tracking deferred work

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ModelValidationTests: GGML magic byte validation (valid, truncated,
  wrong magic, missing file, empty file)
- TextCorrectorTests: refusal detection, buildInstructions for all app
  categories, edge cases (nil bundleID, nil both, original matches)
- UpdateCheckerTests: semver comparison (newer major/minor/patch, equal,
  older, different lengths, four segments)
- ModelSizeTests: displayName, sizeDescription, allCases, Identifiable
- Fix BenchmarkTests for updated TextCorrector.correct() signature

47 tests, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	FreeWispr/Sources/FreeWispr/TextCorrector.swift
#	FreeWispr/Sources/FreeWispr/WhisperTranscriber.swift
#	scripts/build-and-notarize.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 23, 2026 05:13
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR ships FreeWispr v1.3.0 with a focus on improving transcription quality, resilience (timeouts/cancellation, model validation), update security (DMG signature verification), and user accessibility (recording indicator, status signaling, VoiceOver announcements).

Changes:

  • Added LLM text correction (Apple Intelligence) with app-aware context and a 5s timeout; added Whisper inference timeout/cancellation handling.
  • Improved robustness: audio validation + normalization, GGML model validation with auto-delete on corruption, safer resource bundle lookup, and hardened build/notarization steps.
  • Added UI/accessibility enhancements: floating recording indicator, richer status dot semantics, and VoiceOver announcements.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
VERSION Bumps version to 1.3.0.
TODOS.md Adds post-v1.3 WER measurement corpus task.
scripts/build-and-notarize.sh Adds resource bundle presence check + DMG notarization + Gatekeeper verification steps.
FreeWispr/Tests/FreeWisprTests/UpdateCheckerTests.swift Adds semver comparison tests and updateAvailable default test.
FreeWispr/Tests/FreeWisprTests/TextCorrectorTests.swift Adds tests for refusal detection and app-context instruction building.
FreeWispr/Tests/FreeWisprTests/ModelValidationTests.swift Adds GGML magic-byte validation tests and corruption auto-delete checks.
FreeWispr/Tests/FreeWisprTests/FreeWisprTests.swift Extends enum coverage tests (ModelDownloadError, ModelSize).
FreeWispr/Tests/BenchmarkTests/PipelineBenchmark.swift Updates TextCorrector API usage in benchmark.
FreeWispr/Sources/FreeWispr/WhisperTranscriber.swift Adds 30s inference timeout with cancellation and timeout error mapping.
FreeWispr/Sources/FreeWispr/UpdateChecker.swift Adds Team ID–based DMG signature verification prior to opening updates; exposes semver helper for tests.
FreeWispr/Sources/FreeWispr/TextCorrector.swift Adds app-aware LLM correction with 5s timeout and refusal detection; removes frontmost-app querying from corrector.
FreeWispr/Sources/FreeWispr/RecordingIndicator.swift Introduces floating recording indicator dot with Reduce Motion support.
FreeWispr/Sources/FreeWispr/ModelManager.swift Adds GGML validation post-download and new corrupted-model error.
FreeWispr/Sources/FreeWispr/FreeWisprApp.swift Adds status dot color mapping and VoiceOver announcement behavior.
FreeWispr/Sources/FreeWispr/BundleExtension.swift Improves resource bundle fallback chain and avoids crashes if missing.
FreeWispr/Sources/FreeWispr/AudioRecorder.swift Adds hardware config change detection + engine rebuild, and a thread-safe capture flag for the tap callback.
FreeWispr/Sources/FreeWispr/AppState.swift Adds audio validation/normalization, recording indicator integration, temporary error messaging, and revised model-switch flow.
CLAUDE.md Adds guidance on gstack browsing skill usage.
CHANGELOG.md Adds v1.3.0 changelog entry.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +79 to +83
} catch is CancellationError {
throw TranscriberError.timeout
} catch WhisperError.cancelled {
throw TranscriberError.timeout
} catch {
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the timeout/cancellation paths, timeoutTask is never cancelled. If whisper.transcribe throws CancellationError or WhisperError.cancelled, the timeout task can keep running and later call whisper.cancel(), potentially cancelling a subsequent transcription. Consider cancelling timeoutTask in all exit paths (e.g., via defer { timeoutTask.cancel() } right after creation).

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +176
// Mount the DMG
let mountProcess = Process()
mountProcess.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
mountProcess.arguments = ["attach", dmgPath.path, "-nobrowse", "-readonly", "-plist"]
let pipe = Pipe()
mountProcess.standardOutput = pipe

do {
try mountProcess.run()
mountProcess.waitUntilExit()
} catch {
logger.error("Failed to mount DMG: \(error.localizedDescription)")
return false
}

guard mountProcess.terminationStatus == 0 else {
logger.error("hdiutil attach failed with exit code \(mountProcess.terminationStatus)")
return false
}

// Parse mount point from plist output
let outputData = pipe.fileHandleForReading.readDataToEndOfFile()
guard let plist = try? PropertyListSerialization.propertyList(from: outputData, format: nil) as? [String: Any],
let entities = plist["system-entities"] as? [[String: Any]],
let mountPoint = entities.first(where: { $0["mount-point"] != nil })?["mount-point"] as? String
else {
logger.error("Could not parse DMG mount point")
return false
}

defer {
// Always detach the DMG
let detach = Process()
detach.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
detach.arguments = ["detach", mountPoint, "-quiet"]
try? detach.run()
detach.waitUntilExit()
}

// Find .app bundle inside the mount point
let mountURL = URL(fileURLWithPath: mountPoint)
guard let contents = try? FileManager.default.contentsOfDirectory(at: mountURL, includingPropertiesForKeys: nil),
let appURL = contents.first(where: { $0.pathExtension == "app" })
else {
logger.error("No .app found in mounted DMG")
return false
}

// Verify code signature
var staticCode: SecStaticCode?
guard SecStaticCodeCreateWithPath(appURL as CFURL, [], &staticCode) == errSecSuccess,
let code = staticCode else {
logger.error("Cannot create static code reference for \(appURL.lastPathComponent)")
return false
}

// Validate the signature is valid
let requirement = "anchor apple generic and certificate leaf[subject.OU] = \"\(expectedTeamID)\""
var secRequirement: SecRequirement?
guard SecRequirementCreateWithString(requirement as CFString, [], &secRequirement) == errSecSuccess,
let req = secRequirement else {
logger.error("Cannot create security requirement")
return false
}

let status = SecStaticCodeCheckValidityWithErrors(code, SecCSFlags(rawValue: kSecCSCheckAllArchitectures), req, nil)
if status == errSecSuccess {
logger.info("Update signature verified: Team ID \(expectedTeamID) matches")
return true
} else {
logger.error("Signature verification failed (status: \(status)) for \(appURL.lastPathComponent)")
return false
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verifyDMGSignature is awaited from @MainActor code, but it uses synchronous Process.waitUntilExit() and blocking reads, which can freeze the UI while mounting and verifying the DMG. Consider running the whole verification in a detached/background task (or using async Process handling) so downloadAndInstall() remains responsive.

Suggested change
// Mount the DMG
let mountProcess = Process()
mountProcess.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
mountProcess.arguments = ["attach", dmgPath.path, "-nobrowse", "-readonly", "-plist"]
let pipe = Pipe()
mountProcess.standardOutput = pipe
do {
try mountProcess.run()
mountProcess.waitUntilExit()
} catch {
logger.error("Failed to mount DMG: \(error.localizedDescription)")
return false
}
guard mountProcess.terminationStatus == 0 else {
logger.error("hdiutil attach failed with exit code \(mountProcess.terminationStatus)")
return false
}
// Parse mount point from plist output
let outputData = pipe.fileHandleForReading.readDataToEndOfFile()
guard let plist = try? PropertyListSerialization.propertyList(from: outputData, format: nil) as? [String: Any],
let entities = plist["system-entities"] as? [[String: Any]],
let mountPoint = entities.first(where: { $0["mount-point"] != nil })?["mount-point"] as? String
else {
logger.error("Could not parse DMG mount point")
return false
}
defer {
// Always detach the DMG
let detach = Process()
detach.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
detach.arguments = ["detach", mountPoint, "-quiet"]
try? detach.run()
detach.waitUntilExit()
}
// Find .app bundle inside the mount point
let mountURL = URL(fileURLWithPath: mountPoint)
guard let contents = try? FileManager.default.contentsOfDirectory(at: mountURL, includingPropertiesForKeys: nil),
let appURL = contents.first(where: { $0.pathExtension == "app" })
else {
logger.error("No .app found in mounted DMG")
return false
}
// Verify code signature
var staticCode: SecStaticCode?
guard SecStaticCodeCreateWithPath(appURL as CFURL, [], &staticCode) == errSecSuccess,
let code = staticCode else {
logger.error("Cannot create static code reference for \(appURL.lastPathComponent)")
return false
}
// Validate the signature is valid
let requirement = "anchor apple generic and certificate leaf[subject.OU] = \"\(expectedTeamID)\""
var secRequirement: SecRequirement?
guard SecRequirementCreateWithString(requirement as CFString, [], &secRequirement) == errSecSuccess,
let req = secRequirement else {
logger.error("Cannot create security requirement")
return false
}
let status = SecStaticCodeCheckValidityWithErrors(code, SecCSFlags(rawValue: kSecCSCheckAllArchitectures), req, nil)
if status == errSecSuccess {
logger.info("Update signature verified: Team ID \(expectedTeamID) matches")
return true
} else {
logger.error("Signature verification failed (status: \(status)) for \(appURL.lastPathComponent)")
return false
}
return await Task.detached(priority: .userInitiated) { () -> Bool in
// Mount the DMG
let mountProcess = Process()
mountProcess.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
mountProcess.arguments = ["attach", dmgPath.path, "-nobrowse", "-readonly", "-plist"]
let pipe = Pipe()
mountProcess.standardOutput = pipe
do {
try mountProcess.run()
mountProcess.waitUntilExit()
} catch {
logger.error("Failed to mount DMG: \(error.localizedDescription)")
return false
}
guard mountProcess.terminationStatus == 0 else {
logger.error("hdiutil attach failed with exit code \(mountProcess.terminationStatus)")
return false
}
// Parse mount point from plist output
let outputData = pipe.fileHandleForReading.readDataToEndOfFile()
guard let plist = try? PropertyListSerialization.propertyList(from: outputData, format: nil) as? [String: Any],
let entities = plist["system-entities"] as? [[String: Any]],
let mountPoint = entities.first(where: { $0["mount-point"] != nil })?["mount-point"] as? String
else {
logger.error("Could not parse DMG mount point")
return false
}
defer {
// Always detach the DMG
let detach = Process()
detach.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
detach.arguments = ["detach", mountPoint, "-quiet"]
try? detach.run()
detach.waitUntilExit()
}
// Find .app bundle inside the mount point
let mountURL = URL(fileURLWithPath: mountPoint)
guard let contents = try? FileManager.default.contentsOfDirectory(at: mountURL, includingPropertiesForKeys: nil),
let appURL = contents.first(where: { $0.pathExtension == "app" })
else {
logger.error("No .app found in mounted DMG")
return false
}
// Verify code signature
var staticCode: SecStaticCode?
guard SecStaticCodeCreateWithPath(appURL as CFURL, [], &staticCode) == errSecSuccess,
let code = staticCode else {
logger.error("Cannot create static code reference for \(appURL.lastPathComponent)")
return false
}
// Validate the signature is valid
let requirement = "anchor apple generic and certificate leaf[subject.OU] = \"\(expectedTeamID)\""
var secRequirement: SecRequirement?
guard SecRequirementCreateWithString(requirement as CFString, [], &secRequirement) == errSecSuccess,
let req = secRequirement else {
logger.error("Cannot create security requirement")
return false
}
let status = SecStaticCodeCheckValidityWithErrors(code, SecCSFlags(rawValue: kSecCSCheckAllArchitectures), req, nil)
if status == errSecSuccess {
logger.info("Update signature verified: Team ID \(expectedTeamID) matches")
return true
} else {
logger.error("Signature verification failed (status: \(status)) for \(appURL.lastPathComponent)")
return false
}
}.value

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
// Read _isCapturing under bufferQueue to avoid data race
let capturing = self.bufferQueue.sync { self._isCapturing }
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The audio tap callback uses bufferQueue.sync to read _isCapturing. Because the tap runs on a real-time audio thread, synchronously blocking on a dispatch queue can cause audio glitches/dropouts under load. Consider using an atomic/lock-free flag (or another RT-safe mechanism) so the tap can check capture state without blocking.

Suggested change
// Read _isCapturing under bufferQueue to avoid data race
let capturing = self.bufferQueue.sync { self._isCapturing }
// Read _isCapturing directly to avoid blocking the real-time audio thread.
// A slight race here is acceptable: at worst, a few extra frames are processed
// just before/after recording state changes.
let capturing = self._isCapturing

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +8
private var panel: NSPanel?
private var pulseTimer: Timer?

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pulseTimer is declared/invalidated but never set; pulsing is driven by a Core Animation on the layer instead. Consider removing pulseTimer (and related invalidation) to avoid dead code and confusion, or wire it up if a timer-based pulse was intended.

Copilot uses AI. Check for mistakes.
Comment on lines +143 to +148
// Find .app bundle inside the mount point
let mountURL = URL(fileURLWithPath: mountPoint)
guard let contents = try? FileManager.default.contentsOfDirectory(at: mountURL, includingPropertiesForKeys: nil),
let appURL = contents.first(where: { $0.pathExtension == "app" })
else {
logger.error("No .app found in mounted DMG")
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DMG verification picks the first .app found in the mounted root. If the DMG contains multiple apps (or a malicious extra app), this could verify the wrong bundle. Consider selecting the expected app explicitly (e.g., by matching FreeWispr.app) and failing if it's not present.

Suggested change
// Find .app bundle inside the mount point
let mountURL = URL(fileURLWithPath: mountPoint)
guard let contents = try? FileManager.default.contentsOfDirectory(at: mountURL, includingPropertiesForKeys: nil),
let appURL = contents.first(where: { $0.pathExtension == "app" })
else {
logger.error("No .app found in mounted DMG")
// Find expected .app bundle inside the mount point
let mountURL = URL(fileURLWithPath: mountPoint)
let expectedAppName = "FreeWispr.app"
guard let contents = try? FileManager.default.contentsOfDirectory(at: mountURL, includingPropertiesForKeys: nil),
let appURL = contents.first(where: { $0.lastPathComponent == expectedAppName })
else {
logger.error("Expected app bundle \(expectedAppName, privacy: .public) not found in mounted DMG")

Copilot uses AI. Check for mistakes.
}

// Validate the signature is valid
let requirement = "anchor apple generic and certificate leaf[subject.OU] = \"\(expectedTeamID)\""
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security requirement only constrains Team ID (certificate leaf[subject.OU]), so any app signed by the same Team ID would pass. Consider tightening the requirement to also include the expected bundle identifier (or using the app’s designated requirement) to reduce the chance of accepting the wrong signed app.

Suggested change
let requirement = "anchor apple generic and certificate leaf[subject.OU] = \"\(expectedTeamID)\""
let expectedBundleID = "com.ygivenx.FreeWispr"
let requirement = "anchor apple generic and certificate leaf[subject.OU] = \"\(expectedTeamID)\" and info[CFBundleIdentifier] = \"\(expectedBundleID)\""

Copilot uses AI. Check for mistakes.
Comment on lines +84 to 88
if !isTapInstalled || needsRebuild {
resetEngine()
try prepareEngine()
needsRebuild = false
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needsRebuild is read/reset in startRecording() without any synchronization, but it’s also mutated from the engine configuration-change notification. Since that notification may be delivered off the main thread, this can introduce a data race. Consider protecting needsRebuild behind a queue/lock/atomic or ensuring mutations happen on a single actor/queue.

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +120
@objc private func handleConfigurationChange(_ notification: Notification) {
// Mark for rebuild — don't tear down now since we may be mid-recording.
// The tap will be rebuilt on the next startRecording() call.
needsRebuild = true
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleConfigurationChange sets needsRebuild = true directly. If this callback is invoked on a non-main thread (likely), it can race with startRecording() reading/resetting the same flag. Consider hopping to a single queue/actor before mutating shared state, or making needsRebuild atomic.

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +128
// Parse mount point from plist output
let outputData = pipe.fileHandleForReading.readDataToEndOfFile()
guard let plist = try? PropertyListSerialization.propertyList(from: outputData, format: nil) as? [String: Any],
let entities = plist["system-entities"] as? [[String: Any]],
let mountPoint = entities.first(where: { $0["mount-point"] != nil })?["mount-point"] as? String
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If parsing the hdiutil attach -plist output fails, the function returns before setting up the detach defer, leaving the DMG mounted. Consider attaching with a known mountpoint (e.g., -mountpoint <tempDir>) or capturing a device identifier so you can always detach even when parsing fails.

Copilot uses AI. Check for mistakes.
ygivenx and others added 2 commits March 22, 2026 22:25
The magic number 0x67676D6C was checked in big-endian byte order, but
GGML files store it little-endian. This caused every downloaded model
to be rejected as corrupted and deleted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ygivenx ygivenx merged commit 44a24a0 into main Mar 23, 2026
1 check passed
@ygivenx ygivenx deleted the feat/v1.3-quality branch March 23, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants