ocrs

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: Apache-2.0 Imports: 3 Imported by: 0

README

OCRS

ocrs is a small Go module for running OCR providers behind reusable handler contracts. It was extracted from an internal RAG OCR package so other Go libraries can use the PaddleOCR client, structured result parsing, chunked PDF processing, retry middleware, and provider dispatching without depending on the RAG codebase.

Install

go get github.com/eruca/goagents/ocrs

For local development before publishing:

go mod edit -replace github.com/eruca/goagents/ocrs=/Users/nick/VibeCoding/goagents/ocrs
go get github.com/eruca/goagents/ocrs

Packages

  • ocrs: generic Handler, Middleware, OCR, provider contracts, and common result types.
  • paddleocr: PaddleOCR HTTP API client, response DTOs, structured chunk parsing, and chunk result merging.
  • chunking: PDF chunk orchestration and a pdfcpu splitter.
  • scheduler: worker dispatcher for routing calls across OCR providers.
  • retrypolicy: generic retry middleware with bounded exponential backoff.

Use With goagent

ocrs is an OCR capability module. goagent is an agent runtime framework. Keep the dependency direction at the application boundary:

your application
  imports github.com/eruca/goagents/goagent
  imports github.com/eruca/goagents/ocrs
  wraps ocrs as one or more goagent tools

github.com/eruca/goagents/goagent does not import github.com/eruca/goagents/ocrs
github.com/eruca/goagents/ocrs does not import github.com/eruca/goagents/goagent

This keeps goagent generic and keeps ocrs usable by non-agent programs. The application decides which OCR actions are exposed to the model, how files are authorized, and how much OCR output is returned to the LLM context.

Recommended tool shape:

type OCRDocumentTool struct {
	Handler ocrs.Handler[[]byte, ocrs.OCRResult]
}

func (t OCRDocumentTool) Spec() tools.Spec {
	return tools.Spec{
		Name:        "ocr_document",
		Description: "Extracts structured text from an approved PDF or image document.",
		Permission:  policy.PermissionRead,
		Schema: tools.Schema{
			JSONSchema: json.RawMessage(`{
				"type":"object",
				"properties":{
					"document_id":{"type":"string"}
				},
				"required":["document_id"]
			}`),
		},
	}
}

func (t OCRDocumentTool) Execute(ctx context.Context, input json.RawMessage, env tools.Env) (*tools.Result, error) {
	var req struct {
		DocumentID string `json:"document_id"`
	}
	if err := json.Unmarshal(input, &req); err != nil {
		return nil, err
	}

	// Resolve document_id through your application storage or request metadata.
	// Avoid letting the model pass arbitrary filesystem paths.
	data, err := loadDocumentBytes(ctx, req.DocumentID, env)
	if err != nil {
		return nil, err
	}

	result, err := t.Handler.Handle(ctx, data)
	if err != nil {
		return nil, err
	}
	title, chunks, err := paddleocr.ParseStructuredChunks(result.Raw)
	if err != nil {
		return nil, err
	}

	preview := firstChunkTexts(chunks, 3)
	return &tools.Result{
		ForLLM: fmt.Sprintf("OCR title: %s\nChunks: %d\nPreview:\n%s", title, len(chunks), preview),
		ForUser: string(result.Raw),
	}, nil
}

Register that tool with a normal goagent registry:

client := paddleocr.NewClient("paddleocr", apiURL, token, 10*time.Minute)
handler := chunking.NewCutAwareHandler(chunking.CutAwareConfig{
	PagesPerChunk:    20,
	ChunkConcurrency: 2,
	Splitter:         chunking.PDFCPUSplitter{},
	Inner:            client,
	Merge:            paddleocr.MergeChunkResponses,
})

registry := tools.NewRegistry()
registry.Register(OCRDocumentTool{Handler: handler})

agent, err := agentcore.NewAgent(
	agentcore.WithLLM(llm),
	agentcore.WithToolRegistry(registry),
)

Use document_id or another host-owned identifier instead of raw paths when possible. OCR output can be much larger than an LLM context window, so return a bounded preview in ForLLM and keep full structured JSON in ForUser, object storage, or a retrieval index.

PaddleOCR Client

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/eruca/goagents/ocrs/paddleocr"
)

func main() {
	apiURL := os.Getenv("PADDLEOCR_API_URL")
	token := os.Getenv("PADDLEOCR_TOKEN")
	pdfBytes, err := os.ReadFile("document.pdf")
	if err != nil {
		panic(err)
	}

	client := paddleocr.NewClient("paddleocr", apiURL, token, 10*time.Minute)
	result, err := client.Handle(context.Background(), pdfBytes)
	if err != nil {
		panic(err)
	}

	title, chunks, err := paddleocr.ParseStructuredChunks(result.Raw)
	if err != nil {
		panic(err)
	}
	fmt.Println(title, len(chunks))
}

PaddleOCR responses are returned as ocrs.OCRResult. Raw contains the original provider JSON so callers can either parse provider-specific details or use paddleocr.ParseStructuredChunks.

Multiple Tokens

client := paddleocr.NewClientWithTokens(
	"paddleocr",
	apiURL,
	[]string{"token-a", "token-b"},
	10*time.Minute,
)

The token pool rotates tokens and marks quota-like HTTP statuses as exhausted for the current Asia/Shanghai day.

Chunk Large PDFs

handler := chunking.NewCutAwareHandler(chunking.CutAwareConfig{
	PagesPerChunk:    20,
	ChunkConcurrency: 2,
	Splitter:         chunking.PDFCPUSplitter{},
	Inner:            client,
	Merge:            paddleocr.MergeChunkResponses,
})

result, err := handler.Handle(ctx, pdfBytes)

Non-PDF input bypasses chunking. If PDF splitting fails, CutAwareHandler falls back to direct OCR on the original bytes.

Examples

go run ./examples/parse
go run ./examples/chunked

PADDLEOCR_API_URL=https://example.com/paddleocr \
PADDLEOCR_TOKEN=token \
OCR_FILE=/path/to/document.pdf \
go run ./examples/paddleocr

examples/paddleocr exits successfully with a skip message when required environment variables are missing.

Verify

go test ./...

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Handler

type Handler[I, O any] interface {
	Handle(ctx context.Context, data I) (O, error)
}

type HandlerFunc

type HandlerFunc[I, O any] func(ctx context.Context, data I) (O, error)

func (HandlerFunc[I, O]) Handle

func (f HandlerFunc[I, O]) Handle(ctx context.Context, data I) (O, error)

type Lease

type Lease[I, O any] interface {
	Endpoint() ProviderConfig[I, O]
	Done(success bool)
}

type Middleware

type Middleware[I, O any] interface {
	Wrap(next Handler[I, O]) Handler[I, O]
}

func Chain

func Chain[I, O any](middlewares ...Middleware[I, O]) Middleware[I, O]

type MiddlewareFunc

type MiddlewareFunc[I, O any] func(next Handler[I, O]) Handler[I, O]

func (MiddlewareFunc[I, O]) Wrap

func (f MiddlewareFunc[I, O]) Wrap(next Handler[I, O]) Handler[I, O]

type OCR

type OCR[T any] struct {
	// contains filtered or unexported fields
}

func New

func New[T any](handler Handler[[]byte, T], middlewares ...Middleware[[]byte, T]) *OCR[T]

func NewWithCloser

func NewWithCloser[T any](handler Handler[[]byte, T], closer io.Closer, middlewares ...Middleware[[]byte, T]) *OCR[T]

func (*OCR[T]) Close

func (o *OCR[T]) Close() error

func (*OCR[T]) Handle

func (o *OCR[T]) Handle(ctx context.Context, data []byte) (T, error)

type OCRResult

type OCRResult struct {
	Provider string          `json:"provider"`
	Raw      json.RawMessage `json:"raw"`
}

type ProcessedChunk

type ProcessedChunk struct {
	Index int    `json:"index"`
	Text  string `json:"text"`
}

type Provider

type Provider[I, O any] interface {
	Family() string
	Name() string
	Handler[I, O]
}

type ProviderConfig

type ProviderConfig[I, O any] struct {
	Provider Provider[I, O]
	Workers  int
}

type Scheduler

type Scheduler[I, O any] interface {
	Acquire(ctx context.Context) (Lease[I, O], error)
}

type UnprocessedChunk

type UnprocessedChunk struct {
	Title   string `json:"title"`
	Content string `json:"content"`
	Skipped bool   `json:"skipped"`
}

Directories

Path Synopsis
examples
chunked command
paddleocr command
parse command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL