← Back to list

Building Privacy-First OCR Redaction with Apple Vision and Apple Intelligence

A practical journey from “copy text from screenshots” to “review sensitive text before sharing.”

Doran Gao · 2026-05-25 04:53 · 0 claps · 14.0 min read paywalled
#apple-vision #apple-intelligence #swift #macos-development #privacy-engineering
Open on Medium ↗
Wiki topics: 3D · Motion & 3D Design 📱 · Mobile Development 🔒 · Cybersecurity

Building Privacy-First OCR Redaction with Apple Vision and Apple Intelligence

A practical journey from “copy text from screenshots” to “review sensitive text before sharing.”

https://theonequote.app/q/3qc

https://theonequote.app/q/3qc

Security is a process, not a product. — Bruce Schneier

That line is a useful lens for building AI features inside privacy-sensitive software.

It is tempting to frame AI as the feature: send the screenshot to a model, ask what should be hidden, and let the model decide. But redaction is not a prompt. It is a trust workflow.

A screenshot can contain email addresses, API keys, customer names, internal hostnames, tickets, tokens, URLs, and all the tiny contextual details we stop noticing after staring at a screen all day. The job is not merely to recognize those details. The job is to help the user review them before anything leaves their machine.

That tension is what pushed OneMark, my native macOS screenshot markup app (coming soon), toward a smarter OCR workflow.

I did not want to build a chat feature. I wanted something quieter and more useful:

  1. Recognize text in the screenshot locally.
  2. Detect obvious sensitive text deterministically.
  3. Let Apple Intelligence add context when it is available.
  4. Keep the user in control before any redaction is applied.
  5. Prepare an explicit local handoff bundle only when the user chooses to share with an external AI tool.

The result is OCR / Smart Redaction: Apple Vision does the OCR, local rules create a reliable baseline, Apple Intelligence provides optional Smart Review suggestions through Foundation Models, and OneMark turns accepted suggestions into normal editable redaction rectangles.

No screenshot, OCR text, annotation, or handoff bundle is uploaded by OneMark in this release.

Thesis: In a privacy-first app, AI should not be the center of the product. It should be one bounded participant in a chain of evidence, review, and user intent.

The Shape of the Workflow

Before getting into implementation details, here is the mental model:

This is the core architecture of the feature.

Apple Vision owns text recognition and geometry. Local rules own the baseline. Apple Intelligence owns optional context. The user owns the final action.

Why OCR First, AI Second

The first design decision was to avoid asking a language model to “look at the screenshot” directly.

OneMark is an annotation app, so the key unit of action is not a paragraph. It is geometry: a rectangle on a canvas that can be reviewed, moved, resized, saved, undone, and burned into exported output.

Apple Vision OCR gives the app the factual layer it needs:

  • recognized strings
  • confidence values
  • text bounding boxes
  • range-level geometry when available

Apple Intelligence then reviews text and candidate metadata, not raw pixels.

That keeps the model’s job narrow: classify likely sensitive OCR substrings, explain them, and suggest missing items. It also keeps the app honest, because every actionable AI suggestion must map back to OCR geometry before OneMark can draw a redaction.

The model can suggest. It cannot draw a box unless the app can ground that suggestion in OCR geometry.

Step 1: Make OCR a Value-Producing Service

The OCR service is deliberately small.

It accepts a CGImage and the current document snapshot, runs Vision off the main actor, and returns value types that the rest of the app can reason about.

import CoreGraphics
import Foundation
import Vision

struct OCRTextRecognitionService {
    func recognizeText(
        in image: CGImage,
        document: AnnotationDocument
    ) async throws -> OCRTextRecognitionResult {
        let imageSize = CGSize(width: image.width, height: image.height)
        let baseImageOrigin = document.baseImageOrigin

        return try await Task.detached(priority: .userInitiated) {
            let request = VNRecognizeTextRequest()
            request.recognitionLevel = .accurate
            request.usesLanguageCorrection = true
            request.automaticallyDetectsLanguage = true

            let handler = VNImageRequestHandler(cgImage: image, options: [:])
            try handler.perform([request])

            let textResults = (request.results ?? [])
                .compactMap { observation -> OCRTextResult? in
                    guard let candidate = observation.topCandidates(1).first else {
                        return nil
                    }

                    let rect = OCRTextGeometry.imageRect(
                        fromVisionNormalizedRect: observation.boundingBox,
                        imageSize: imageSize,
                        baseImageOrigin: baseImageOrigin
                    )

                    return OCRTextResult(
                        id: UUID(),
                        text: candidate.string,
                        confidence: candidate.confidence,
                        rect: rect,
                        rangeRects: []
                    )
                }

            return OCRTextRecognitionResult(textResults: textResults)
        }
        .value
    }
}

The production code trims whitespace, captures range rectangles through VNRecognizedText.boundingBox(for:), and sorts results into reading order.

The important architectural point is that OCR returns a plain OCRTextRecognitionResult, not UI state.

That result has one job: preserve recognized text and geometry.

struct OCRTextRecognitionResult: Equatable, Sendable {
    var textResults: [OCRTextResult]

    var hasText: Bool {
        !textResults.isEmpty
    }

    var copiedText: String {
        textResults
            .map(\.text)
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
            .joined(separator: "\n")
    }
}

By keeping this result simple, the rest of the system can treat OCR as a stable input instead of a UI side effect.

Step 2: Get Coordinate Conversion Right

Vision returns normalized rectangles with an origin at the lower-left of the image.

OneMark’s canvas uses pixel coordinates with an origin at the top-left. On top of that, the canvas can expand around the base screenshot when annotations cross an edge, so OCR rectangles must be offset by the base image origin.

This little conversion is one of the most important parts of the whole feature:

enum OCRTextGeometry {
    static func imageRect(
        fromVisionNormalizedRect rect: CGRect,
        imageSize: CGSize,
        baseImageOrigin: CGPoint
    ) -> CGRect {
        CGRect(
            x: rect.minX * imageSize.width + baseImageOrigin.x,
            y: (1 - rect.maxY) * imageSize.height + baseImageOrigin.y,
            width: rect.width * imageSize.width,
            height: rect.height * imageSize.height
        )
        .integral
    }
}

If this is wrong, everything else feels haunted.

The model can be correct. The regex can be correct. The UI can be polished. But the final redaction still lands in the wrong place.

I added tests for normalized Vision rectangles, expanded canvas offsets, padding, copied text order, and range fallback behavior.

For OCR-driven redaction, geometry tests are product tests.

Step 3: Keep Deterministic Rules as the Base Layer

Before Apple Intelligence enters the flow, OneMark scans recognized text with local rules.

This catches the boring-but-important patterns:

  • email addresses
  • phone-like numbers
  • SSN-like values
  • credit-card-like numbers with Luhn validation
  • IPv4 addresses
  • tokens
  • API keys, passwords, and secret assignment patterns

The detector is pure Swift. It does not know about SwiftUI, AppKit, Vision requests, or Apple Intelligence. It receives OCR result values and returns redaction candidates.

struct SensitiveTextDetector {
    static let maxCandidates = 250

    func candidates(
        in recognitionResult: OCRTextRecognitionResult,
        documentSize: CGSize,
        padding: CGSize = OCRTextGeometry.redactionPadding
    ) -> [SensitiveTextCandidate] {
        var candidates: [SensitiveTextCandidate] = []
        var seenKeys = Set<String>()

        for textResult in recognitionResult.textResults {
            for rule in Self.detectionRules {
                for match in Self.matches(
                    for: rule,
                    in: textResult,
                    documentSize: documentSize,
                    padding: padding
                ) {
                    let key = Self.duplicateKey(for: match)
                    guard seenKeys.insert(key).inserted else {
                        continue
                    }

                    candidates.append(match)
                }
            }
        }

        return Array(candidates.sorted(by: Self.candidateSort).prefix(Self.maxCandidates))
    }
}

This layer matters for trust.

If Apple Intelligence is unavailable, disabled, still preparing its model, over budget, or simply fails, OneMark still has useful local OCR and rule-based redaction.

Step 4: Treat Apple Intelligence as Optional

Apple Intelligence is not available on every Mac, every OS version, or every user setting.

The app should not make that feel broken.

So OneMark isolates availability behind a tiny provider:

import Foundation

#if canImport(FoundationModels)
import FoundationModels
#endif

struct AppleIntelligenceAvailability: AppleIntelligenceAvailabilityProviding {
    func currentState() -> AppleIntelligenceAvailabilityState {
        #if canImport(FoundationModels)
        if #available(macOS 26.0, *) {
            switch SystemLanguageModel.default.availability {
            case .available:
                return .available
            case .unavailable(.appleIntelligenceNotEnabled):
                return .disabled
            case .unavailable(.deviceNotEligible):
                return .unavailable(.deviceNotEligible)
            case .unavailable(.modelNotReady):
                return .unavailable(.modelNotReady)
            @unknown default:
                return .unavailable(.frameworkUnavailable)
            }
        }
        return .unavailable(.unsupportedOS)
        #else
        return .unavailable(.frameworkUnavailable)
        #endif
    }
}

The UI does not need to know about framework imports or OS gates.

It receives one of these states:

enum AppleIntelligenceAvailabilityState: Equatable, Sendable {
    case available
    case disabled
    case unavailable(AppleIntelligenceUnavailableReason)
    case failed
    case inputTooLarge
}

That distinction lets the review pane say useful things:

  • Apple Intelligence is off. Turn it on in System Settings to use Smart Review.
  • Smart Review unavailable. Using local redaction rules.
  • Smart Review skipped because recognized text is too long. Local suggestions are still available.
  • Smart Review could not finish. Local suggestions are still available.

The fallback is not an afterthought. It is part of the feature.

Step 5: Ask the Model for Structured Review, Not Prose

OneMark’s Smart Review request contains only text and metadata:

struct SmartReviewRequest: Equatable, Sendable {
    var recognitionResult: OCRTextRecognitionResult
    var ruleCandidates: [SensitiveTextCandidate]
    var documentSize: CGSize
    var sourceContext: SmartReviewSourceContext
}

The prompt is intentionally plain:

num SmartReviewPromptBuilder {
    static var instructions: String {
        """
        You are Smart Review inside OneMark, a privacy-first screenshot markup app.
        Classify OCR text that may need redaction before sharing.
        Return actual sensitive text items only. Never repeat field descriptions, type choices, schema examples, or example values.
        For deterministic candidates, preserve obvious rule risk: Social Security Number, Credit Card, Token, Secret, password, or API key are critical/high.
        Use exact OCR substrings and exact deterministic candidate UUIDs when a suggestion corresponds to a listed candidate.
        Use short user-facing reasons.
        """
    }

    static func prompt(for request: SmartReviewRequest) -> String {
        """
        Review this OCR text and the deterministic redaction candidates.

        Source context: \(request.sourceContext.rawValue)

        OCR text:
        \(request.recognitionResult.copiedText)

        Deterministic candidates:
        \(candidatePromptLines(for: request.ruleCandidates))

        Return one item for each deterministic candidate that should remain redacted, plus any high-confidence sensitive OCR substrings not already listed.
        """
    }
}

The model client then requests schema-constrained output from Foundation Models:

struct FoundationModelsSmartReviewModelClient: SmartReviewModelClient {
    func suggestions(for request: SmartReviewRequest) async throws -> [SmartReviewSuggestion] {
        #if canImport(FoundationModels)
        if #available(macOS 26.0, *) {
            let session = LanguageModelSession(
                instructions: SmartReviewPromptBuilder.instructions
            )
            let response = try await session.respond(
                to: SmartReviewPromptBuilder.prompt(for: request),
                schema: try Self.responseSchema(),
                includeSchemaInPrompt: true,
                options: GenerationOptions(sampling: .greedy, temperature: 0)
            )
            return try SmartReviewSuggestionJSONDecoder.decodeSuggestions(
                from: response.content.jsonString
            )
        }
        #endif

        return []
    }
}

The output shape is not “a helpful explanation.”

It is a list of bounded items:

struct SmartReviewSuggestion: Identifiable, Codable, Equatable, Sendable {
    var id: UUID
    var candidateID: UUID?
    var matchedText: String
    var category: SmartReviewCategory
    var severity: SmartReviewSeverity
    var reason: String
    var confidence: Float
}

This is the difference between “AI in the app” and “AI as a service boundary.”

The app can test the boundary, version the prompt, reject bad output, merge good output, and keep the UI predictable.

Step 6: Budget Before You Generate

Screenshots can contain a lot of text.

A settings window is small. A browser full of logs is not.

OneMark now computes a Smart Review budget before calling the model. When token APIs are available, it counts the prompt and instructions against the model context size. If that fails or the APIs are unavailable, it falls back to a conservative character limit.

struct SmartReviewInputBudgeter: SmartReviewInputBudgeting {
    static let reservedResponseTokens = 768

    func budget(
        for request: SmartReviewRequest,
        prompt: SmartReviewPromptContent
    ) async -> SmartReviewInputBudget {
        #if canImport(FoundationModels)
        if #available(macOS 26.4, *) {
            do {
                let model = SystemLanguageModel.default
                let instructionTokens = try await model.tokenCount(
                    for: Instructions(prompt.instructions)
                )
                let promptTokens = try await model.tokenCount(
                    for: Prompt(prompt.prompt)
                )
                let totalTokens = instructionTokens + promptTokens
                let contextSize = model.contextSize

                return SmartReviewInputBudget(
                    strategy: .tokenAware,
                    characterCount: request.recognitionResult.copiedText.count,
                    tokenCount: totalTokens,
                    contextSize: contextSize,
                    canReview: totalTokens + Self.reservedResponseTokens <= contextSize
                )
            } catch {
                return characterFallbackBudget(for: request)
            }
        }
        #endif

        return characterFallbackBudget(for: request)
    }
}

The practical win is simple: oversized OCR still produces recognized text and rule suggestions, but the app skips model review before wasting time or surfacing a vague failure.

Step 7: Merge AI with Rules, But Never Let AI Delete Rules

The merge policy is intentionally conservative:

  • Rule candidates cannot be removed by Smart Review.
  • Smart Review can add category, severity, reason, and confidence to a rule candidate.
  • Smart Review-only suggestions must map back to OCR geometry.
  • Uncertain or low-confidence items start unchecked.
  • Candidate count stays bounded.
enum SmartReviewMergePolicy {
    static func merge(
        ruleCandidates: [SensitiveTextCandidate],
        suggestions: [SmartReviewSuggestion],
        recognitionResult: OCRTextRecognitionResult,
        documentSize: CGSize
    ) -> [SensitiveTextCandidate] {
        var merged = ruleCandidates.map(normalizedRuleCandidate)
        var candidateIndexesByID = Dictionary(
            uniqueKeysWithValues: merged.enumerated().map { ($0.element.id, $0.offset) }
        )

        for suggestion in suggestions {
            if let candidateID = suggestion.candidateID,
               let index = candidateIndexesByID[candidateID] {
                apply(suggestion, to: &merged[index])
                continue
            }

            guard let candidate = actionableCandidate(
                from: suggestion,
                recognitionResult: recognitionResult,
                documentSize: documentSize
            ) else {
                continue
            }

            merged.append(candidate)
        }

        return Array(merged.sorted(by: candidateSort).prefix(SensitiveTextDetector.maxCandidates))
    }
}

The most important line is this:

guard let candidate = actionableCandidate(...)

That line means a language model cannot create a redaction rectangle out of thin air. It must point to text that Vision actually found.

Smart Review can enrich a rule match, but it cannot erase one.

Step 8: Coordinate the Async Flow Without Stale Results

The editor snapshots the current image and document state before doing OCR or model work.

It also creates an OCRScanToken that includes document identity, content revision, image size, base image origin, and a unique scan ID.

Then the flow can safely run asynchronous work and only publish results if the visible document still matches the token.

func scanOCR() {
    guard let importedImage, let store else {
        statusMessage = "Open or paste an image first"
        return
    }

    let document = store.document
    let token = OCRScanToken(
        documentID: document.id,
        contentRevision: store.contentRevision,
        imagePixelSize: document.imagePixelSize,
        baseImageOrigin: document.baseImageOrigin,
        scanID: UUID()
    )

    let baseImage = importedImage.cgImage
    let documentSize = document.imagePixelSize
    activeOCRScanToken = token
    smartRedactionState.startScanning(token: token)

    Task {
        let result = try await ocrService.recognizeText(
            in: baseImage,
            document: document
        )

        let ruleCandidates = await Task.detached(priority: .userInitiated) {
            SensitiveTextDetector.detect(
                in: result,
                documentSize: documentSize
            )
        }.value

        let smartReviewResponse = await smartReviewService.suggestions(
            for: SmartReviewRequest(
                recognitionResult: result,
                ruleCandidates: ruleCandidates,
                documentSize: documentSize,
                sourceContext: currentImageSourceContext
            )
        )

        let candidates = await Task.detached(priority: .userInitiated) {
            SmartReviewMergePolicy.merge(
                ruleCandidates: ruleCandidates,
                suggestions: smartReviewResponse.suggestions,
                recognitionResult: result,
                documentSize: documentSize
            )
        }.value

        await MainActor.run {
            guard isShowingSmartRedactionReview,
                  currentOCRScanToken == token else { return }

            smartRedactionState.completeScan(
                token: token,
                result: result,
                candidates: candidates,
                smartReviewAvailability: smartReviewResponse.availability,
                smartReviewDiagnostics: smartReviewResponse.diagnostics
            )
        }
    }
}

That token guard prevents a nasty class of bugs:

Start OCR. Replace the image. Receive stale redaction candidates for the old screenshot.

Without the token, the UI could look correct while applying boxes to the wrong content.

With the token, stale work dies quietly.

Step 9: Make Review Explicit

The UI does not auto-redact.

It shows:

  • a Review tab with grouped candidates
  • a Text tab with recognized OCR text
  • source badges: Rule, Smart Review, or Rule + Smart Review
  • a compact summary of whether Apple Intelligence reviewed the result
  • counts for rule-only, combined, and Smart Review-only matches

Accepted candidates become the same editable redaction annotations users can already draw by hand.

func applySmartRedactions() {
    guard smartRedactionState.activeToken == currentOCRScanToken else {
        statusMessage = "Run OCR again before applying redactions."
        return
    }
    guard let store else {
        statusMessage = "Open or paste an image first"
        return
    }

    let rects = smartRedactionState.selectedCandidates.map(\.redactionRect)
    let redactionIDs = store.addRedactions(rects)

    if redactionIDs.count == rects.count {
        smartRedactionState.markSelectedRedactionsApplied()
        retargetOCRScanTokenAfterApplyingRedactions(to: store)
    }

    statusMessage = "Applied \(redactionIDs.count) redactions."
}

This kept the feature aligned with the rest of OneMark.

OCR does not create a special kind of redaction. It creates ordinary annotations that work with undo, save, export, copy, share, and drag.

Step 10: Add an Explicit AI Handoff, Not an Upload

Once OCR and Smart Review existed, the next obvious question was:

“Can this help when I want to ask ChatGPT, Claude, Codex, or another AI tool about this screenshot?”

The answer was yes, but only with a privacy gate.

OneMark’s Create AI Handoff… action creates a local folder:

redacted-image.png
recognized-text.txt
prompt.md
context.json

It does not upload the files.

The user inspects them and chooses what to share.

The manifest intentionally records whether OneMark uploaded anything externally:

struct AIHandoffManifest: Codable, Equatable, Sendable {
    struct Privacy: Codable, Equatable, Sendable {
        var exportedByUser: Bool
        var externalUploadPerformedByOneMark: Bool
    }

    var bundleVersion: String
    var createdAt: String
    var app: String
    var sourceContext: SmartReviewSourceContext?
    var documentPixelSize: PixelSize
    var recognizedText: RecognizedText
    var redactions: Redactions
    var smartReview: SmartReview?
    var privacy: Privacy
}

The generated prompt also tells the external AI not to reconstruct hidden content:

static func promptText(for input: AIHandoffBundleInput) -> String {
    """
    Please help me analyze this redacted screenshot.

    I prepared this local bundle in OneMark before sharing it externally with ChatGPT, Claude, Codex, or another AI tool. Use redacted-image.png as the visual reference and recognized-text.txt as OCR text. Do not try to infer or reconstruct hidden sensitive information behind redactions.

    Goal:
    - Summarize what the screenshot shows.
    - Identify useful next steps.
    - Call out any remaining privacy or security concerns visible in the redacted material.
    """
}

One important caveat is documented in the app: recognized-text.txt can include text that was visible when OCR ran, even if the flattened image now has redaction boxes.

That is why the handoff is local and inspectable.

What Made This Work

The final implementation is less about “adding AI” and more about respecting boundaries.

Vision owns text and geometry.

OCR gives the app real coordinates. Without geometry, there is no actionable redaction.

Rules own the baseline.

Deterministic detection catches known sensitive patterns and keeps the feature useful without Apple Intelligence.

Apple Intelligence owns context.

Smart Review can explain, classify, group, and notice context-sensitive items, but it cannot silently remove rule matches or apply redactions.

The user owns the final action.

OneMark suggests. The user reviews. Redactions are editable annotations.

External AI starts after a local privacy checkpoint.

The AI handoff bundle is an opt-in folder, not a network request.

Testing the Trust Contract

The tests ended up documenting the product philosophy:

  • unavailable Apple Intelligence returns a rule-only response
  • disabled Apple Intelligence shows a settings message
  • model failure keeps local suggestions available
  • oversized OCR skips the model call
  • prompt version and source context are stable
  • Smart Review-only suggestions need OCR geometry to become actionable
  • uncertain Smart Review suggestions start unchecked
  • Vision coordinates convert correctly into OneMark canvas coordinates
  • recognized text preserves reading order
  • docs and in-app Help stay aligned with the feature

The most valuable tests were not just “does this function return data?”

They were tests around trust:

@Test func suggestionWithoutOCRGeometryIsNotActionable() {
    let suggestion = SmartReviewSuggestion(
        id: UUID(),
        candidateID: nil,
        matchedText: "missing.example",
        category: .technical,
        severity: .medium,
        reason: "May reveal infrastructure",
        confidence: 0.92
    )

    let merged = SmartReviewMergePolicy.merge(
        ruleCandidates: [],
        suggestions: [suggestion],
        recognitionResult: recognitionResult("No matching text here"),
        documentSize: CGSize(width: 500, height: 120)
    )

    #expect(merged.isEmpty)
}

That test says: the model may suggest something, but the app will not draw a box unless the suggestion can be grounded in OCR.

Key Takeaways

  • Start with OCR geometry, not AI interpretation. A redaction feature needs boxes users can inspect and edit.
  • Keep deterministic rules as the baseline. The feature should still work when Apple Intelligence is unavailable.
  • Treat the model as a bounded reviewer. Ask for structured suggestions, not open-ended prose.
  • Merge conservatively. AI can add context, but it should not silently remove rule-based findings.
  • Make sharing explicit. A local handoff bundle is safer than an invisible upload.

Lessons Learned

If I were starting this again, I would keep the same order:

  1. Build OCR as a service with geometry.
  2. Convert coordinates early and test them.
  3. Add deterministic rules before AI.
  4. Add Apple Intelligence behind a narrow availability-aware service.
  5. Ask for structured output.
  6. Budget before model generation.
  7. Merge conservatively.
  8. Keep review explicit.
  9. Add external AI handoff only after redaction.

That order made the feature feel native instead of bolted on.

Apple Intelligence became an assistive layer in a workflow that already made sense.

The bigger lesson: for a privacy-sensitive app, AI should not be the center of the product. It should be one carefully bounded participant in a chain of evidence, review, and user intent.

In a privacy-sensitive app, AI should be helpful without becoming invisible authority.

Demo

In the demo, the workflow starts with a screenshot that contains several types of sensitive text: an email address, a token-like value, an internal URL, and a few normal labels that should stay visible.

OneMark first runs OCR with Apple Vision and draws the recognized text regions back onto the canvas. Then the local rule layer marks obvious sensitive patterns immediately. If Apple Intelligence is available, Smart Review adds context, severity, and short reasons for items that may need attention.

The important part is what does not happen automatically.

The app does not redact everything by itself. It opens a review panel where each suggestion can be checked, unchecked, inspected, and edited. Once the user accepts the suggestions, OneMark converts them into regular editable redaction rectangles.

From there, the user can still move, resize, undo, save, export, or create a local AI handoff bundle.

That is the demo version of the whole design principle: AI helps the workflow, but the user still owns the final action.

Before You Go

AI only gets real when you stop talking about it and start building with it. Used well, it unlocks what wasn’t possible before — and as it evolves, it keeps opening new paths and redefining how we do the old ones. That’s what I share here — what works, what breaks, and what’s worth understanding more deeply. **Follow along and subscribe** if you want to stay close to the edge.

[embed]About — Doran Gao — Medium Read writing from Doran Gao on Medium. Doran Gao builds AI-powered products and systems. Creator of TheOneQuote.app…medium.com

References


메타데이터
post_id
20ab80659f89
slug
building-privacy-first-ocr-redaction-with-apple-vision-and-apple-intelligence-20ab80659f89
url
https://medium.com/@dorangao/building-privacy-first-ocr-redaction-with-apple-vision-and-apple-intelligence-20ab80659f89
canonical_url
https://medium.com/@dorangao/building-privacy-first-ocr-redaction-with-apple-vision-and-apple-intelligence-20ab80659f89
author_url
https://medium.com/@dorangao
status
ok
fetched_at
2026-06-20 20:29:01