feat: Add .env.example configuration file and integrate Redis service into Docker Compose for improved task management. Update desktop application to support document indexing and extraction workflows, enhancing user experience with a new extraction wizard and stepper components. Revise README and documentation for clarity on setup and usage.

This commit is contained in:
erik
2026-07-05 22:39:07 -05:00
parent 9aca926988
commit d95f8f6ad8
70 changed files with 9807 additions and 499 deletions
@@ -0,0 +1,99 @@
package ocr
import (
"bytes"
"fmt"
"image"
"image/png"
"github.com/gen2brain/go-fitz"
"github.com/otiai10/gosseract/v2"
)
type Box struct {
Text string
X int
Y int
W int
H int
}
// RenderPage rasterizes the first page of a PDF.
func RenderPage(pdfData []byte, dpi int) (image.Image, int, int, error) {
doc, err := fitz.NewFromMemory(pdfData)
if err != nil {
return nil, 0, 0, fmt.Errorf("open pdf: %w", err)
}
defer doc.Close()
if doc.NumPage() == 0 {
return nil, 0, 0, fmt.Errorf("pdf has no pages")
}
img, err := doc.ImageDPI(0, float64(dpi))
if err != nil {
return nil, 0, 0, fmt.Errorf("render page: %w", err)
}
bounds := img.Bounds()
return img, bounds.Dx(), bounds.Dy(), nil
}
// OCRImage runs Tesseract word-level OCR on a rendered page image.
func (e *Engine) OCRImage(img image.Image) ([]Box, error) {
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return nil, fmt.Errorf("encode png: %w", err)
}
client := e.Acquire()
defer e.Release(client)
if err := client.SetImageFromBytes(buf.Bytes()); err != nil {
return nil, fmt.Errorf("tesseract image: %w", err)
}
boxes, err := client.GetBoundingBoxes(gosseract.RIL_WORD)
if err != nil {
return nil, fmt.Errorf("tesseract ocr: %w", err)
}
out := make([]Box, 0, len(boxes))
for _, b := range boxes {
text := trim(b.Word)
if text == "" || b.Confidence <= 0 {
continue
}
out = append(out, Box{
Text: text,
X: b.Box.Min.X,
Y: b.Box.Min.Y,
W: b.Box.Dx(),
H: b.Box.Dy(),
})
}
return out, nil
}
// ExtractBoxes renders the first PDF page and runs OCR with a pooled Tesseract client.
func (e *Engine) ExtractBoxes(pdfData []byte, dpi int) ([]Box, int, int, error) {
img, pageW, pageH, err := RenderPage(pdfData, dpi)
if err != nil {
return nil, 0, 0, err
}
boxes, err := e.OCRImage(img)
if err != nil {
return nil, 0, 0, err
}
return boxes, pageW, pageH, nil
}
func trim(s string) string {
start, end := 0, len(s)
for start < end && (s[start] == ' ' || s[start] == '\t' || s[start] == '\n') {
start++
}
for end > start && (s[end-1] == ' ' || s[end-1] == '\t' || s[end-1] == '\n') {
end--
}
return s[start:end]
}
@@ -0,0 +1,69 @@
package ocr
import (
"fmt"
"github.com/otiai10/gosseract/v2"
)
// Engine holds a pool of reusable Tesseract clients.
type Engine struct {
pool chan *gosseract.Client
clients []*gosseract.Client
lang string
}
// NewEngine warms up poolSize Tesseract clients for the given language.
func NewEngine(lang string, poolSize int) (*Engine, error) {
if poolSize < 1 {
poolSize = 1
}
e := &Engine{
pool: make(chan *gosseract.Client, poolSize),
clients: make([]*gosseract.Client, 0, poolSize),
lang: lang,
}
for i := 0; i < poolSize; i++ {
client, err := newPooledClient(lang)
if err != nil {
e.Close()
return nil, fmt.Errorf("tesseract client %d: %w", i, err)
}
e.clients = append(e.clients, client)
e.pool <- client
}
return e, nil
}
func newPooledClient(lang string) (*gosseract.Client, error) {
client := gosseract.NewClient()
if err := client.SetLanguage(lang); err != nil {
if err2 := client.SetLanguage("eng"); err2 != nil {
_ = client.Close()
return nil, fmt.Errorf("tesseract lang: %w", err)
}
}
return client, nil
}
// Acquire returns a Tesseract client from the pool.
func (e *Engine) Acquire() *gosseract.Client {
return <-e.pool
}
// Release returns a client to the pool.
func (e *Engine) Release(client *gosseract.Client) {
e.pool <- client
}
// Close shuts down all pooled clients.
func (e *Engine) Close() error {
for _, client := range e.clients {
_ = client.Close()
}
e.clients = nil
return nil
}