Skip to content
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ let package = Package(
.executableTarget(name: "ImageSortDemo", dependencies: ["ImageSort"], exclude: ["README.md"]),
.executableTarget(name: "KevCheck", dependencies: ["FluidUse"]),
.executableTarget(name: "ClefVisionCheck", dependencies: ["FluidUse"]),
.executableTarget(name: "UnlimitedOcrCheck", dependencies: ["FluidUse"]),
.executableTarget(name: "UnlimitedOcrDemo", dependencies: ["FluidUse"], exclude: ["README.md"]),
.executableTarget(
name: "ClefFlashDemo", dependencies: ["FluidUse"], exclude: ["README.md", "demo.sh", "bars.py"]),
.executableTarget(name: "ClefTextCheck", dependencies: ["FluidUse"]),
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,27 @@ print(draft.reply, draft.timing.totalSeconds) // "Cong
`Sources/ShortReplyDemo/demo.sh --x` launches it with a macmon + log terminal
([Sources/ShortReplyDemo](Sources/ShortReplyDemo/README.md)).

## Document OCR (multi-page)

`UnlimitedOcrManager` runs Baidu's [Unlimited-OCR](https://huggingface.co/FluidInference/unlimited-ocr-coreml)
(DeepSeek-OCR architecture, 3B MoE / ~0.5B active, MIT) on the GPU through Core ML: up to 8 document pages go in as
one prompt and the whole document comes out of a single generation as markdown with layout boxes. Its R-SWA cache
(full prefix + a ring of the last 128 generated tokens) is Core ML state, so speed stays flat over a long read. The
pinned, checksummed bundle (~3.8 GB) downloads from the Hub on first use (macOS 15 / iOS 18).

```swift
let ocr = try await UnlimitedOcrManager.load(from: try await UnlimitedOcrModelStore.ensure())
let pages = try (0..<min(pdf.pageCount, ocr.maxPages)).map { try UnlimitedOcrPagePreprocessor.page(from: pdf.page(at: $0)!) }
var text = ""
for try await event in ocr.read(pages: pages) { if case .text(let delta) = event { text += delta } }
print(UnlimitedOcrDocument(parsing: text).markdown)
```

Token-for-token identical to upstream's fp32 `generate` on Baidu's paper (865 tokens for one page, 2,461 for three
pages in one pass). On an M5 Pro: ~100 ms per page to encode, ~21 ms per output token; the first 8 pages of the paper
(6,727 tokens) take 160 s. `UnlimitedOcrDemo` reads a dropped PDF and highlights the block being written on each
page ([Sources/UnlimitedOcrDemo](Sources/UnlimitedOcrDemo/README.md)).

## Demo

```bash
Expand Down
93 changes: 93 additions & 0 deletions Sources/FluidUse/UnlimitedOcr/UnlimitedOcrDocument.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import CoreGraphics
import Foundation

/// Parsed model output: `<PAGE>` sections of `<|det|>label [x1, y1, x2, y2]<|/det|>content` blocks. Boxes are in
/// upstream's 0...999 page coordinates (top-left origin). Parsing is tolerant of a partially streamed tail.
public struct UnlimitedOcrDocument: Sendable {
public struct Block: Sendable, Identifiable {
public let id: Int
public let label: String
/// Normalized 0...1 rectangles (top-left origin); usually one.
public let boxes: [CGRect]
public let content: String
}

public struct Page: Sendable, Identifiable {
public let id: Int
public let blocks: [Block]
}

public let pages: [Page]

public init(parsing text: String) {
var pages: [Page] = []
var sections = text.components(separatedBy: "<PAGE>")
if sections.first?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true { sections.removeFirst() }
var blockID = 0
for (pageIndex, section) in sections.enumerated() {
var blocks: [Block] = []
let parts = section.components(separatedBy: "<|det|>")
for part in parts.dropFirst() {
let pieces = part.components(separatedBy: "<|/det|>")
let header = pieces[0]
let content = pieces.count > 1 ? pieces[1...].joined(separator: "") : ""
let label =
header.split(separator: "[", maxSplits: 1).first.map {
$0.trimmingCharacters(in: .whitespaces)
} ?? ""
blocks.append(
Block(
id: blockID, label: label, boxes: Self.boxes(in: header),
content: content.trimmingCharacters(in: .whitespacesAndNewlines)))
blockID += 1
}
if blocks.isEmpty, !section.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
blocks.append(
Block(
id: blockID, label: "text", boxes: [],
content: section.trimmingCharacters(in: .whitespacesAndNewlines)))
blockID += 1
}
pages.append(Page(id: pageIndex, blocks: blocks))
}
self.pages = pages
}

/// Every complete `[x1, y1, x2, y2]` group in a det header, normalized.
static func boxes(in header: String) -> [CGRect] {
var rects: [CGRect] = []
var numbers: [Double] = []
var current = ""
var inside = false
for character in header {
switch character {
case "[":
inside = true
numbers = []
current = ""
case "]":
if let value = Double(current.trimmingCharacters(in: .whitespaces)) { numbers.append(value) }
if inside, numbers.count == 4 {
rects.append(
CGRect(
x: numbers[0] / 999, y: numbers[1] / 999, width: (numbers[2] - numbers[0]) / 999,
height: (numbers[3] - numbers[1]) / 999))
}
inside = false
current = ""
case ",":
if let value = Double(current.trimmingCharacters(in: .whitespaces)) { numbers.append(value) }
current = ""
default:
if inside { current.append(character) }
}
}
return rects
}

/// Plain markdown: layout markup removed, pages separated by a rule.
public var markdown: String {
pages.map { page in page.blocks.map(\.content).filter { !$0.isEmpty }.joined(separator: "\n\n") }
.joined(separator: "\n\n---\n\n")
}
}
Loading
Loading