Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/MobileOCR.dpr
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
program MobileOCR;

uses
uniGUIApplication,
uMainForm in 'uMainForm.pas' {MainmForm},
uMainModule in 'uMainModule.pas' {MainModule: TUniGUIMainModule},
uServerModule in 'uServerModule.pas' {UniServerModule: TUniServerModule};

{$R *.res}

begin
ReportMemoryLeaksOnShutdown := True;
if IsLibrary then
uniGUIServerModule.Initialize;
if WebMode then
UniServerModule.Start;
UniGUIServerModuleInstance := TUniServerModule.Create(nil);
try
UniGUIServerModuleInstance.InitApplication;
UniGUIServerModuleInstance.LoadConfig;
UniGUIServerModuleInstance.Run;
finally
UniGUIServerModuleInstance.Free;
end;
end.
43 changes: 43 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Lector de etiquetas para UniGUI Mobile

Este ejemplo para Delphi 10.4 y UniGUI Mobile permite capturar una foto de la etiqueta de un lote y extraer los datos relevantes (lote, fecha, cooperativa, etc.) aprovechando la API de ChatGPT para visión.

## Componentes principales

- **`uMainForm`**: formulario móvil que presenta el botón de captura y muestra el resultado.
- **`uMainModule`**: módulo principal que lee la configuración y expone el método `RecognizeLabel` para enviar la imagen a la API de ChatGPT.
- **`uLabelParser`**: clase auxiliar que interpreta el JSON retornado por ChatGPT y extrae los campos clave.
- **`uServerModule`**: configuración del servidor UniGUI.

## Configuración

1. Copia `config.ini.example` a la carpeta `files` en el servidor UniGUI y renómbralo a `config.ini`.
2. Edita los valores de `apiKey` y, opcionalmente, `model`/`baseUrl` con tus credenciales de OpenAI.

```
[openai]
apiKey=sk-...
model=gpt-4o-mini
baseUrl=https://api.openai.com/v1/chat/completions
```

> `baseUrl` es opcional; deja el valor por defecto para utilizar el endpoint oficial de OpenAI.

## Flujo de funcionamiento

1. El usuario pulsa **"Capturar etiqueta"** y el componente `TUnimFileUpload` abre la cámara del móvil.
2. Tras tomar la foto, el flujo `UploadCompleted` guarda la imagen en memoria y llama a `MainModule.RecognizeLabel`.
3. El módulo envía la imagen a la API de ChatGPT y obtiene una respuesta estructurada en JSON.
4. `TLabelParser.ParseChatGPTResponse` asigna los valores al registro `TLabelInfo`.
5. El formulario muestra los datos en un `TUnimMemo`.

## Personalización

- Ajusta el parser para reconocer otros campos del etiquetado específico de tu cooperativa.
- Añade validaciones o almacenamiento en base de datos desde `TMainModule` para registrar los lotes.
- Cambia el modelo de ChatGPT modificando los valores del `config.ini`.

## Requisitos

- Delphi 10.4 con UniGUI Mobile (probado con la versión 1.90.1552).
- Cuenta de OpenAI con acceso a modelos con capacidad de visión.
4 changes: 4 additions & 0 deletions src/config.ini.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[openai]
apiKey=
model=gpt-4o-mini
baseUrl=https://api.openai.com/v1/chat/completions
118 changes: 118 additions & 0 deletions src/uLabelParser.pas
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
unit uLabelParser;

interface

uses
System.SysUtils,
System.Classes,
System.JSON,
System.Generics.Collections;

type
TLabelInfo = record
Lot: string;
HarvestDate: string;
Cooperative: string;
Product: string;
Category: string;
BunchCount: string;
procedure Clear;
end;

TLabelParser = class
public
class function ParseChatGPTResponse(AJSON: TJSONObject): TLabelInfo;
end;

implementation

{ TLabelInfo }

procedure TLabelInfo.Clear;
begin
Lot := '';
HarvestDate := '';
Cooperative := '';
Product := '';
Category := '';
BunchCount := '';
end;

function GetCaseInsensitiveValue(AJSON: TJSONObject; const AName: string): string;
var
LPair: TJSONPair;
LValue: TJSONValue;
begin
Result := '';
if AJSON = nil then
Exit;

for LPair in AJSON do
begin
if SameText(LPair.JsonString.Value, AName) then
begin
LValue := LPair.JsonValue;
if LValue is TJSONString then
Exit(TJSONString(LValue).Value)
else
Exit(LValue.ToJSON);
end;
end;
end;

function ExtractNestedObject(AJSON: TJSONObject; const AName: string): TJSONObject;
var
LValue: TJSONValue;
begin
Result := nil;
if AJSON = nil then
Exit(nil);

LValue := AJSON.Values[AName];
if LValue is TJSONObject then
Result := TJSONObject(LValue)
else
Result := nil;
end;

class function TLabelParser.ParseChatGPTResponse(AJSON: TJSONObject): TLabelInfo;
var
LResult: TLabelInfo;
LData: TJSONObject;
begin
LResult.Clear;
if AJSON = nil then
Exit(LResult);

LData := ExtractNestedObject(AJSON, 'data');
if (LData <> nil) and (LData <> AJSON) then
Exit(ParseChatGPTResponse(LData));

LResult.Lot := GetCaseInsensitiveValue(AJSON, 'lot');
if LResult.Lot = '' then
LResult.Lot := GetCaseInsensitiveValue(AJSON, 'lote');

LResult.HarvestDate := GetCaseInsensitiveValue(AJSON, 'harvest_date');
if LResult.HarvestDate = '' then
LResult.HarvestDate := GetCaseInsensitiveValue(AJSON, 'fecha');

LResult.Cooperative := GetCaseInsensitiveValue(AJSON, 'cooperative');
if LResult.Cooperative = '' then
LResult.Cooperative := GetCaseInsensitiveValue(AJSON, 'cooperativa');

LResult.Product := GetCaseInsensitiveValue(AJSON, 'product');
if LResult.Product = '' then
LResult.Product := GetCaseInsensitiveValue(AJSON, 'producto');

LResult.Category := GetCaseInsensitiveValue(AJSON, 'category');
if LResult.Category = '' then
LResult.Category := GetCaseInsensitiveValue(AJSON, 'categoria');

LResult.BunchCount := GetCaseInsensitiveValue(AJSON, 'bunch_count');
if LResult.BunchCount = '' then
LResult.BunchCount := GetCaseInsensitiveValue(AJSON, 'bultos');

Result := LResult;
end;

end.
38 changes: 38 additions & 0 deletions src/uMainForm.dfm
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
object MainmForm: TMainmForm
Caption = 'Lector de Etiquetas'
OnCreate = UnimFormCreate
object pnlHeader: TUnimPanel
Align = alTop
Height = 48
TabOrder = 0
object lblTitle: TUnimLabel
Align = alClient
Caption = 'Lector de Lotes'
end
end
object pnlContent: TUnimPanel
Align = alClient
TabOrder = 1
object btnCapture: TUnimButton
Align = alTop
Height = 60
Caption = 'Capturar etiqueta'
OnClick = btnCaptureClick
TabOrder = 0
end
object Upload: TUnimFileUpload
Left = 0
Top = 64
Width = 100
Height = 32
Visible = False
OnCompleted = UploadCompleted
end
object memResult: TUnimMemo
Align = alClient
ReadOnly = True
EmptyText = 'Los resultados aparecerán aquí'
TabOrder = 1
end
end
end
140 changes: 140 additions & 0 deletions src/uMainForm.pas
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
unit uMainForm;

interface

uses
System.SysUtils,
System.Classes,
System.JSON,
uniGUIBaseClasses,
uniGUIClasses,
uniGUImForm,
uniGUIApplication,
UnimFileUpload,
UnimPanel,
UnimLabel,
UnimButton,
UnimMemo,
uLabelParser;

type
TMainmForm = class(TUnimForm)
pnlHeader: TUnimPanel;
lblTitle: TUnimLabel;
pnlContent: TUnimPanel;
btnCapture: TUnimButton;
memResult: TUnimMemo;
Upload: TUnimFileUpload;
procedure UnimFormCreate(Sender: TObject);
procedure btnCaptureClick(Sender: TObject);
procedure UploadCompleted(Sender: TObject); override;
private
procedure AnalyzeStream(AStream: TStream);
procedure ShowResult(const AInfo: TLabelInfo);
procedure ShowError(const AMessage: string);
public
end;

function MainmForm: TMainmForm;

implementation

{$R *.dfm}

uses
uMainModule;

procedure TMainmForm.AnalyzeStream(AStream: TStream);
var
LJSON: TJSONObject;
LInfo: TLabelInfo;
begin
memResult.Lines.Clear;
if AStream = nil then
begin
ShowError('No se recibió imagen.');
Exit;
end;

AStream.Position := 0;
LJSON := MainModule.RecognizeLabel(AStream);
try
if LJSON = nil then
ShowError('No fue posible contactar con el servicio de análisis.')
else
begin
LInfo := TLabelParser.ParseChatGPTResponse(LJSON);
ShowResult(LInfo);
end;
finally
LJSON.Free;
end;
end;

procedure TMainmForm.btnCaptureClick(Sender: TObject);
begin
Upload.Reset;
Upload.Execute;
end;

procedure TMainmForm.ShowError(const AMessage: string);
begin
memResult.Lines.Text := '⚠ ' + AMessage;
end;

procedure TMainmForm.ShowResult(const AInfo: TLabelInfo);
begin
memResult.Lines.BeginUpdate;
try
memResult.Lines.Clear;
memResult.Lines.Add('Lote: ' + AInfo.Lot);
memResult.Lines.Add('Fecha: ' + AInfo.HarvestDate);
memResult.Lines.Add('Cooperativa: ' + AInfo.Cooperative);
memResult.Lines.Add('Producto: ' + AInfo.Product);
memResult.Lines.Add('Categoría: ' + AInfo.Category);
memResult.Lines.Add('Nº Bultos: ' + AInfo.BunchCount);
finally
memResult.Lines.EndUpdate;
end;
end;

procedure TMainmForm.UnimFormCreate(Sender: TObject);
begin
Caption := 'Lector de Etiquetas';
lblTitle.Caption := 'Lector de Lotes';
btnCapture.Caption := 'Capturar etiqueta';
memResult.EmptyText := 'Los resultados aparecerán aquí';
Upload.Accept := 'image/*';
Upload.Capture := ucCamera;
Upload.MaxFiles := 1;
end;

procedure TMainmForm.UploadCompleted(Sender: TObject);
var
LStream: TMemoryStream;
begin
inherited;
if Upload.Files.Count = 0 then
begin
ShowError('No se seleccionó ninguna imagen.');
Exit;
end;

LStream := TMemoryStream.Create;
try
Upload.Files[0].SaveToStream(LStream);
AnalyzeStream(LStream);
finally
LStream.Free;
end;
end;

function MainmForm: TMainmForm;
begin
Result := TMainmForm(UniMainModule.GetFormInstance(TMainmForm));
end;

initialization
RegisterAppFormClass(TMainmForm);

end.
5 changes: 5 additions & 0 deletions src/uMainModule.dfm
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
object MainModule: TMainModule
OldCreateOrder = False
EnableSynchronousOperations = True
ApplicationTitle = 'Lector de Lotes'
end
Loading