Backend:
- Migration 002: product_recognition_jobs table with JSONB images column
and job_type CHECK ('receipt' | 'products')
- New Kafka topics: ai.products.paid / ai.products.free
- ProductJob model, ProductJobRepository (mirrors dish job pattern)
- itemEnricher extracted from Handler — shared by HTTP handler and worker
- ProductSSEBroker: PG LISTEN on product_job_update channel
- ProductWorkerPool: 5 workers, branches on job_type to call
RecognizeReceipt or RecognizeProducts per image in parallel
- Handler: RecognizeReceipt and RecognizeProducts now return 202 Accepted
instead of blocking; 4 new endpoints: GET /ai/product-jobs,
/product-jobs/history, /product-jobs/{id}, /product-jobs/{id}/stream
- cmd/worker: extended to run ProductWorkerPool alongside dish WorkerPool
- cmd/server: wires productJobRepository + productSSEBroker; both SSE
brokers started in App.Start()
Flutter client:
- ProductJobCreated, ProductJobResult, ProductJobSummary, ProductJobEvent
models + submitReceiptRecognition/submitProductsRecognition/stream methods
- Shared _openSseStream helper eliminates duplicate SSE parsing loop
- ScanScreen: replace blocking AI calls with async submit + navigate to
ProductJobWatchScreen
- ProductJobWatchScreen: watches SSE stream, navigates to /scan/confirm
when done, shows error on failure
- ProductsScreen: prepends _RecentScansSection (hidden when empty); compact
horizontal list of recent scans with "See all" → history
- ProductJobHistoryScreen: full list of all product recognition jobs
- New routes: /scan/product-job-watch, /products/job-history
- L10n: 7 new keys in all 12 ARB files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
64 lines
1.9 KiB
Go
64 lines
1.9 KiB
Go
package recognition
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/food-ai/backend/internal/adapters/ai"
|
|
)
|
|
|
|
// Kafka topic names for product recognition.
|
|
const (
|
|
ProductTopicPaid = "ai.products.paid"
|
|
ProductTopicFree = "ai.products.free"
|
|
)
|
|
|
|
// ProductImagePayload is a single image stored in the product_recognition_jobs.images JSONB column.
|
|
type ProductImagePayload struct {
|
|
ImageBase64 string `json:"image_base64"`
|
|
MimeType string `json:"mime_type"`
|
|
}
|
|
|
|
// ProductJobResultItem is an enriched product item stored in the result JSONB.
|
|
type ProductJobResultItem struct {
|
|
Name string `json:"name"`
|
|
Quantity float64 `json:"quantity"`
|
|
Unit string `json:"unit"`
|
|
Category string `json:"category"`
|
|
Confidence float64 `json:"confidence"`
|
|
MappingID *string `json:"mapping_id,omitempty"`
|
|
StorageDays int `json:"storage_days"`
|
|
}
|
|
|
|
// ProductJobResult is the JSONB payload stored in product_recognition_jobs.result.
|
|
type ProductJobResult struct {
|
|
JobType string `json:"job_type"`
|
|
Items []ProductJobResultItem `json:"items"`
|
|
Unrecognized []ai.UnrecognizedItem `json:"unrecognized,omitempty"`
|
|
}
|
|
|
|
// ProductJob represents an async product/receipt recognition task.
|
|
type ProductJob struct {
|
|
ID string
|
|
UserID string
|
|
UserPlan string
|
|
JobType string // "receipt" | "products"
|
|
Images []ProductImagePayload
|
|
Lang string
|
|
Status string
|
|
Result *ProductJobResult
|
|
Error *string
|
|
CreatedAt time.Time
|
|
StartedAt *time.Time
|
|
CompletedAt *time.Time
|
|
}
|
|
|
|
// ProductJobSummary is a lightweight record for list endpoints (omits image payloads).
|
|
type ProductJobSummary struct {
|
|
ID string `json:"id"`
|
|
JobType string `json:"job_type"`
|
|
Status string `json:"status"`
|
|
Result *ProductJobResult `json:"result,omitempty"`
|
|
Error *string `json:"error,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|