diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..4786e3a --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,25 @@ +FROM golang:1.24-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/server ./cmd/server + +FROM alpine:3.21 + +RUN apk add --no-cache ca-certificates curl + +WORKDIR /app + +COPY --from=builder /bin/server . +COPY internal/db/migrations ./internal/db/migrations + +RUN mkdir -p /app/uploads /data + +EXPOSE 8080 + +CMD ["./server"] diff --git a/backend/internal/handlers/files.go b/backend/internal/handlers/files.go index 9736e30..60defb5 100644 --- a/backend/internal/handlers/files.go +++ b/backend/internal/handlers/files.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin" "github.com/vaultdrop/backend/internal/db" + "github.com/vaultdrop/backend/internal/ocr" "github.com/vaultdrop/backend/internal/service" ) @@ -191,6 +192,8 @@ func (h *Handlers) UploadFiles(c *gin.Context) { Id string `json:"id"` } + client := ocr.NewClient("http://localhost:9090") + filesStats := []FileStats{} for _, file := range files { @@ -260,6 +263,15 @@ func (h *Handlers) UploadFiles(c *gin.Context) { Id: dbFile.ID, }) + text, err := client.Recognize(fileByte) + + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(text) + } c.JSON(http.StatusOK, gin.H{ diff --git a/backend/internal/ocr/client.go b/backend/internal/ocr/client.go new file mode 100644 index 0000000..1fdb66b --- /dev/null +++ b/backend/internal/ocr/client.go @@ -0,0 +1,91 @@ +package ocr + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type Client struct { + endpoint string + httpClient *http.Client +} + +func NewClient(endpoint string) *Client { + return &Client{ + endpoint: endpoint, + httpClient: &http.Client{ + Timeout: 120 * time.Second, + }, + } +} + +func (c *Client) Recognize(imageData []byte) ([]TextBlock, error) { + b64 := base64.StdEncoding.EncodeToString(imageData) + + reqBody, err := json.Marshal(OCRRequest{Image: b64}) + if err != nil { + return nil, fmt.Errorf("marshal request: %w", err) + } + + resp, err := c.httpClient.Post( + c.endpoint+"/ocr", + "application/json", + bytes.NewReader(reqBody), + ) + if err != nil { + return nil, fmt.Errorf("call paddleocr: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("paddleocr returned %d: %s", resp.StatusCode, string(body)) + } + + var ocrResp OCRResponse + if err := json.Unmarshal(body, &ocrResp); err != nil { + return nil, fmt.Errorf("decode response: %w", err) + } + + if ocrResp.ErrorCode != 0 { + return nil, fmt.Errorf("paddleocr error %d: %s", ocrResp.ErrorCode, ocrResp.Message) + } + + return flattenResults(ocrResp.Result), nil +} + +func (c *Client) HealthCheck() error { + resp, err := c.httpClient.Get(c.endpoint + "/health") + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("health check failed: status %d", resp.StatusCode) + } + return nil +} + +func flattenResults(result OCRResult) []TextBlock { + var blocks []TextBlock + for _, page := range result.OCRResults { + for i, text := range page.RecTexts { + score := 0.0 + if i < len(page.RecScores) { + score = page.RecScores[i] + } + blocks = append(blocks, TextBlock{Text: text, Score: score}) + } + } + return blocks +} diff --git a/backend/internal/ocr/types.go b/backend/internal/ocr/types.go new file mode 100644 index 0000000..86874e3 --- /dev/null +++ b/backend/internal/ocr/types.go @@ -0,0 +1,27 @@ +package ocr + +type OCRRequest struct { + Image string `json:"image"` +} + +type OCRResponse struct { + ErrorCode int `json:"errorCode"` + Result OCRResult `json:"result"` + Message string `json:"message,omitempty"` +} + +type OCRResult struct { + OCRResults []OCRPageResult `json:"ocrResults"` +} + +type OCRPageResult struct { + RecTexts []string `json:"rec_texts"` + RecScores []float64 `json:"rec_scores"` + RecBoxes [][]int `json:"rec_boxes"` + RecPolys [][][]int `json:"rec_polys"` +} + +type TextBlock struct { + Text string `json:"text"` + Score float64 `json:"score"` +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..efeaff9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +services: + paddleocr: + image: paddlecloud/paddleocr:2.6-cpu-latest + ports: + - "9090:8080" + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + + # backend: + # build: + # context: ./backend + # dockerfile: Dockerfile + # ports: + # - "8080:8080" + # environment: + # - PORT=8080 + # - DB_PATH=/data/vaultdrop.db + # - OCR_ENDPOINT=http://paddleocr:8080 + # volumes: + # - backend-data:/data + # - ./backend/uploads:/app/uploads + # depends_on: + # paddleocr: + # condition: service_healthy + # restart: unless-stopped + +volumes: + backend-data: