feat(person): projeter explicitement les valeurs OCR confirmées
This commit is contained in:
parent
b8debcde2d
commit
78f0e45450
50 changed files with 2025 additions and 116 deletions
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Projection contrôlée des champs OCR confirmés vers les attributs structurés
|
||||||
|
d’une personne, avec provenance append-only, conflits explicites et rollback.
|
||||||
- Saisie et consultation, depuis la fiche preuve, d’un historique immuable
|
- Saisie et consultation, depuis la fiche preuve, d’un historique immuable
|
||||||
d’appréciations humaines d’authenticité documentaire avec justification et
|
d’appréciations humaines d’authenticité documentaire avec justification et
|
||||||
`OcrRun` facultatif appartenant à la preuve, sans verdict automatique.
|
`OcrRun` facultatif appartenant à la preuve, sans verdict automatique.
|
||||||
|
|
|
||||||
33
Makefile
33
Makefile
|
|
@ -198,6 +198,8 @@ TEST_WORKSPACE_IDENTITY_OCR_GTK := tests/test_workspace_identity_ocr_gtk
|
||||||
TEST_DIALOG_GEOMETRY_GTK := tests/test_dialog_geometry_gtk
|
TEST_DIALOG_GEOMETRY_GTK := tests/test_dialog_geometry_gtk
|
||||||
TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK := tests/test_person_factual_relation_editor_gtk
|
TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK := tests/test_person_factual_relation_editor_gtk
|
||||||
TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK := tests/test_document_authenticity_editor_gtk
|
TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK := tests/test_document_authenticity_editor_gtk
|
||||||
|
TEST_PERSON_OCR_PROJECTION := tests/test_person_ocr_projection
|
||||||
|
TEST_PERSON_OCR_PROJECTION_EDITOR_GTK := tests/test_person_ocr_projection_editor_gtk
|
||||||
FAKE_DOCUMENT_TOOL := tests/fake_document_tool
|
FAKE_DOCUMENT_TOOL := tests/fake_document_tool
|
||||||
|
|
||||||
DOCUMENT_ANALYSIS_TEST_SOURCES := \
|
DOCUMENT_ANALYSIS_TEST_SOURCES := \
|
||||||
|
|
@ -236,6 +238,21 @@ $(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK): \
|
||||||
src/database/error.c src/core/relation_type_normalizer.c
|
src/database/error.c src/core/relation_type_normalizer.c
|
||||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
|
PERSON_OCR_PROJECTION_SOURCES := src/models/person_ocr_projection.c \
|
||||||
|
src/core/person_ocr_projection_mapping.c src/core/person_ocr_projection_service.c \
|
||||||
|
src/dao/person_ocr_projection_dao.c src/dao/identity_ocr_dao.c \
|
||||||
|
src/models/identity_ocr.c src/models/identity_traceability.c \
|
||||||
|
src/dao/entity_dao.c src/models/entity_record.c src/database/database.c \
|
||||||
|
src/database/schema.c src/database/statement.c src/database/transaction.c \
|
||||||
|
src/database/error.c src/core/relation_type_normalizer.c
|
||||||
|
$(TEST_PERSON_OCR_PROJECTION): tests/test_person_ocr_projection.c $(PERSON_OCR_PROJECTION_SOURCES)
|
||||||
|
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||||
|
$(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK): tests/test_person_ocr_projection_editor_gtk.c \
|
||||||
|
src/views/person_ocr_projection_editor.c src/models/person_ocr_projection.c \
|
||||||
|
src/core/person_ocr_projection_mapping.c src/models/identity_ocr.c \
|
||||||
|
src/models/identity_traceability.c
|
||||||
|
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
all: $(TARGET)
|
all: $(TARGET)
|
||||||
|
|
||||||
$(TEST_IDENTITY_OCR): tests/test_identity_ocr.c \
|
$(TEST_IDENTITY_OCR): tests/test_identity_ocr.c \
|
||||||
|
|
@ -958,6 +975,9 @@ $(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
|
||||||
src/views/create_person_dialog.c src/views/dialog_geometry.c \
|
src/views/create_person_dialog.c src/views/dialog_geometry.c \
|
||||||
src/views/person_vocabulary_adapter.c \
|
src/views/person_vocabulary_adapter.c \
|
||||||
src/views/person_factual_relation_editor.c \
|
src/views/person_factual_relation_editor.c \
|
||||||
|
src/views/person_ocr_projection_editor.c src/views/person_creation_confirmation.c \
|
||||||
|
src/models/person_ocr_projection.c \
|
||||||
|
src/core/person_ocr_projection_mapping.c \
|
||||||
src/views/identity_ocr_option_adapter.c \
|
src/views/identity_ocr_option_adapter.c \
|
||||||
src/core/person_dialog_lifecycle.c \
|
src/core/person_dialog_lifecycle.c \
|
||||||
src/core/person_confirmation_summary.c \
|
src/core/person_confirmation_summary.c \
|
||||||
|
|
@ -989,6 +1009,7 @@ $(TEST_CREATE_PERSON_DIALOG_OCR_GTK): \
|
||||||
src/views/create_person_dialog.c src/views/dialog_geometry.c \
|
src/views/create_person_dialog.c src/views/dialog_geometry.c \
|
||||||
src/views/person_vocabulary_adapter.c \
|
src/views/person_vocabulary_adapter.c \
|
||||||
src/views/person_factual_relation_editor.c \
|
src/views/person_factual_relation_editor.c \
|
||||||
|
src/views/person_ocr_projection_editor.c src/views/person_creation_confirmation.c \
|
||||||
src/views/identity_ocr_option_adapter.c \
|
src/views/identity_ocr_option_adapter.c \
|
||||||
src/core/person_dialog_lifecycle.c \
|
src/core/person_dialog_lifecycle.c \
|
||||||
src/core/person_confirmation_summary.c \
|
src/core/person_confirmation_summary.c \
|
||||||
|
|
@ -1000,6 +1021,8 @@ $(TEST_CREATE_PERSON_DIALOG_OCR_GTK): \
|
||||||
src/core/identity_ocr_workflow.c \
|
src/core/identity_ocr_workflow.c \
|
||||||
src/core/identity_field_extractor.c src/core/ocr_analysis.c \
|
src/core/identity_field_extractor.c src/core/ocr_analysis.c \
|
||||||
src/core/person_creation_coordinator.c \
|
src/core/person_creation_coordinator.c \
|
||||||
|
src/models/person_ocr_projection.c src/core/person_ocr_projection_mapping.c \
|
||||||
|
src/core/person_ocr_projection_service.c src/dao/person_ocr_projection_dao.c \
|
||||||
src/core/document_analysis.c src/core/document_tool_runner.c \
|
src/core/document_analysis.c src/core/document_tool_runner.c \
|
||||||
src/core/tool_registry.c src/models/identity_ocr.c \
|
src/core/tool_registry.c src/models/identity_ocr.c \
|
||||||
src/dao/identity_ocr_dao.c src/dao/entity_dao.c \
|
src/dao/identity_ocr_dao.c src/dao/entity_dao.c \
|
||||||
|
|
@ -1056,6 +1079,8 @@ $(TEST_EVIDENCE_STAGING): tests/test_evidence_staging.c \
|
||||||
$(TEST_PERSON_CREATION_COORDINATOR): \
|
$(TEST_PERSON_CREATION_COORDINATOR): \
|
||||||
tests/test_person_creation_coordinator.c \
|
tests/test_person_creation_coordinator.c \
|
||||||
src/core/person_creation_coordinator.c src/core/evidence_staging.c \
|
src/core/person_creation_coordinator.c src/core/evidence_staging.c \
|
||||||
|
src/models/person_ocr_projection.c src/core/person_ocr_projection_mapping.c \
|
||||||
|
src/core/person_ocr_projection_service.c src/dao/person_ocr_projection_dao.c \
|
||||||
src/core/file_hash.c \
|
src/core/file_hash.c \
|
||||||
src/models/identity_ocr.c src/dao/identity_ocr_dao.c \
|
src/models/identity_ocr.c src/dao/identity_ocr_dao.c \
|
||||||
src/models/identity_traceability.c src/dao/identity_traceability_dao.c \
|
src/models/identity_traceability.c src/dao/identity_traceability_dao.c \
|
||||||
|
|
@ -1212,7 +1237,9 @@ test: \
|
||||||
$(TEST_WORKSPACE_IDENTITY_OCR_GTK) \
|
$(TEST_WORKSPACE_IDENTITY_OCR_GTK) \
|
||||||
$(TEST_DIALOG_GEOMETRY_GTK) \
|
$(TEST_DIALOG_GEOMETRY_GTK) \
|
||||||
$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) \
|
$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) \
|
||||||
$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK)
|
$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK) \
|
||||||
|
$(TEST_PERSON_OCR_PROJECTION) \
|
||||||
|
$(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK)
|
||||||
@echo "Exécution des tests..."
|
@echo "Exécution des tests..."
|
||||||
@./$(TEST_NODE)
|
@./$(TEST_NODE)
|
||||||
@./$(TEST_TREE_MODEL)
|
@./$(TEST_TREE_MODEL)
|
||||||
|
|
@ -1310,6 +1337,8 @@ test: \
|
||||||
@$(TEST_DIALOG_GEOMETRY_GTK)
|
@$(TEST_DIALOG_GEOMETRY_GTK)
|
||||||
@$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK)
|
@$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK)
|
||||||
@$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK)
|
@$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK)
|
||||||
|
@$(TEST_PERSON_OCR_PROJECTION)
|
||||||
|
@$(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK)
|
||||||
@echo "Tous les tests sont valides."
|
@echo "Tous les tests sont valides."
|
||||||
|
|
||||||
%.o: %.c
|
%.o: %.c
|
||||||
|
|
@ -1411,6 +1440,8 @@ clean:
|
||||||
$(TEST_DIALOG_GEOMETRY_GTK) \
|
$(TEST_DIALOG_GEOMETRY_GTK) \
|
||||||
$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) \
|
$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) \
|
||||||
$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK) \
|
$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK) \
|
||||||
|
$(TEST_PERSON_OCR_PROJECTION) \
|
||||||
|
$(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK) \
|
||||||
$(FAKE_DOCUMENT_TOOL)
|
$(FAKE_DOCUMENT_TOOL)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ Le projet vise à fournir un environnement local, modulaire et traçable pour or
|
||||||
La fiche d’une preuve permet aussi de consigner une appréciation humaine
|
La fiche d’une preuve permet aussi de consigner une appréciation humaine
|
||||||
d’authenticité documentaire. Cet historique est immuable, justifié selon le
|
d’authenticité documentaire. Cet historique est immuable, justifié selon le
|
||||||
statut choisi et ne peut jamais être produit automatiquement par l’OCR.
|
statut choisi et ne peut jamais être produit automatiquement par l’OCR.
|
||||||
|
Les seules valeurs OCR pouvant alimenter un profil de personne sont celles
|
||||||
|
confirmées par un humain, sélectionnées sans choix par défaut puis validées
|
||||||
|
avec une cible et une stratégie de conflit explicites.
|
||||||
|
La fiche personne relit ensuite ces champs structurés depuis SQLite.
|
||||||
|
|
||||||
> **État du projet : développement actif**
|
> **État du projet : développement actif**
|
||||||
>
|
>
|
||||||
|
|
|
||||||
32
database/schema_v19.sql
Normal file
32
database/schema_v19.sql
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* V19 — attributs structurés et provenance des projections OCR humaines. */
|
||||||
|
CREATE TABLE person_profile_fields(
|
||||||
|
person_id TEXT NOT NULL,field_code TEXT NOT NULL,value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL CHECK(length(updated_at)=20),
|
||||||
|
PRIMARY KEY(person_id,field_code),
|
||||||
|
CHECK(field_code IN('declared_name','surname','given_names','birth_date',
|
||||||
|
'birth_place','nationality','sex_as_printed','address_as_printed')),
|
||||||
|
CHECK(length(trim(value))>0),
|
||||||
|
FOREIGN KEY(person_id) REFERENCES entites(id) ON DELETE CASCADE);
|
||||||
|
CREATE TABLE person_ocr_field_projections(
|
||||||
|
id TEXT PRIMARY KEY,person_id TEXT NOT NULL,person_field_code TEXT NOT NULL,
|
||||||
|
previous_value TEXT,new_value TEXT NOT NULL,evidence_id TEXT NOT NULL,
|
||||||
|
ocr_run_id TEXT NOT NULL,ocr_field_id TEXT NOT NULL,ocr_field_code TEXT NOT NULL,
|
||||||
|
value_quality TEXT NOT NULL CHECK(value_quality IN('complete','partial')),
|
||||||
|
review_status TEXT NOT NULL CHECK(review_status IN('accepted','modified')),
|
||||||
|
strategy TEXT NOT NULL CHECK(strategy IN('fill_empty','replace_existing')),
|
||||||
|
projected_at TEXT NOT NULL CHECK(length(projected_at)=20),
|
||||||
|
origin TEXT NOT NULL CHECK(origin='human'),
|
||||||
|
FOREIGN KEY(person_id) REFERENCES entites(id) ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY(ocr_field_id) REFERENCES identity_field_observations(id) ON DELETE RESTRICT);
|
||||||
|
CREATE INDEX idx_person_projection_person ON person_ocr_field_projections(person_id,projected_at,id);
|
||||||
|
CREATE TRIGGER projection_v19_consistency BEFORE INSERT ON person_ocr_field_projections BEGIN
|
||||||
|
SELECT CASE WHEN NOT EXISTS(SELECT 1 FROM identity_field_observations f
|
||||||
|
WHERE f.id=NEW.ocr_field_id AND f.ocr_run_id=NEW.ocr_run_id
|
||||||
|
AND f.evidence_id=NEW.evidence_id AND f.field_code=NEW.ocr_field_code
|
||||||
|
AND f.confirmation_state='human_confirmed' AND length(trim(f.confirmed_value))>0
|
||||||
|
AND f.review_status IN('accepted','modified')
|
||||||
|
AND f.value_quality IN('complete','partial') AND f.confirmed_value=NEW.new_value)
|
||||||
|
THEN RAISE(ABORT,'OCR field is no longer projectable') END;
|
||||||
|
END;
|
||||||
|
|
@ -128,6 +128,12 @@ l’authenticité à `DocumentAuthenticityEditor`, qui appelle
|
||||||
SQLite. Le service ajoute chaque appréciation humaine dans une transaction et
|
SQLite. Le service ajoute chaque appréciation humaine dans une transaction et
|
||||||
chaîne son identifiant à l’appréciation courante.
|
chaîne son identifiant à l’appréciation courante.
|
||||||
|
|
||||||
|
La V19 ajoute les champs de profil structurés et l’historique des projections
|
||||||
|
OCR humaines. `PersonOcrProjectionEditor` ne sélectionne rien par défaut ; le
|
||||||
|
service relit le champ V18 avant COMMIT et conserve la valeur existante par
|
||||||
|
défaut en cas de conflit. La fiche personne relit ces champs par
|
||||||
|
`PersonDetailsProvider` et le DAO de projection.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Objectif
|
## 1. Objectif
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ L’authenticité documentaire se valide avec
|
||||||
sur un affichage GTK réel avec `G_DEBUG=fatal-criticals`; ses données et sa
|
sur un affichage GTK réel avec `G_DEBUG=fatal-criticals`; ses données et sa
|
||||||
base SQLite sont exclusivement temporaires et marquées `SPECIMEN`.
|
base SQLite sont exclusivement temporaires et marquées `SPECIMEN`.
|
||||||
|
|
||||||
|
La projection OCR se valide avec `test_person_ocr_projection`,
|
||||||
|
`test_person_creation_coordinator` et
|
||||||
|
`test_person_ocr_projection_editor_gtk`, ce dernier sous affichage réel et
|
||||||
|
`G_DEBUG=fatal-criticals`.
|
||||||
|
|
||||||
La validation ciblée de l’aperçu partagé comprend
|
La validation ciblée de l’aperçu partagé comprend
|
||||||
`test_evidence_preview_widget_gtk`, `test_evidence_preview`,
|
`test_evidence_preview_widget_gtk`, `test_evidence_preview`,
|
||||||
`test_evidence_video_preview_controller` et
|
`test_evidence_video_preview_controller` et
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,8 @@ PARTIEL — VALIDATION MANUELLE DES TRANCHES LIVRÉES RÉUSSIE
|
||||||
valeur explicitement confirmée et contraintes anti-automatisme.
|
valeur explicitement confirmée et contraintes anti-automatisme.
|
||||||
- interface GTK de saisie et de consultation de l’authenticité documentaire
|
- interface GTK de saisie et de consultation de l’authenticité documentaire
|
||||||
depuis la fiche preuve, avec OcrRun facultatif filtré et historique immuable.
|
depuis la fiche preuve, avec OcrRun facultatif filtré et historique immuable.
|
||||||
|
- projection humaine explicite des seules valeurs OCR V18 confirmées vers les
|
||||||
|
champs structurés de la personne, avec provenance et rollback V19.
|
||||||
|
|
||||||
Limitations restantes :
|
Limitations restantes :
|
||||||
|
|
||||||
|
|
@ -187,9 +189,7 @@ Limitations restantes :
|
||||||
charge ;
|
charge ;
|
||||||
2. les interfaces GTK complètes des états d’identification et des rôles V18
|
2. les interfaces GTK complètes des états d’identification et des rôles V18
|
||||||
restent à construire ;
|
restent à construire ;
|
||||||
3. la provenance uniforme de tous les dérivés et la projection des seules
|
3. la provenance uniforme de tous les autres dérivés reste partielle ;
|
||||||
valeurs OCR confirmées vers les attributs structurés de la personne restent
|
|
||||||
partielles ;
|
|
||||||
|
|
||||||
Ces limites interdisent de présenter le ticket #109 comme terminé.
|
Ces limites interdisent de présenter le ticket #109 comme terminé.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,13 @@ appartient à la même preuve. `previous_assessment_id` forme une chaîne
|
||||||
append-only ; l’ajout et la détermination du maillon courant sont coordonnés
|
append-only ; l’ajout et la détermination du maillon courant sont coordonnés
|
||||||
dans une transaction.
|
dans une transaction.
|
||||||
|
|
||||||
|
V19 ajoute `person_profile_fields` et `person_ocr_field_projections`. Cette
|
||||||
|
dernière table conserve personne, cible, valeurs précédente et nouvelle,
|
||||||
|
preuve, OcrRun, champ OCR, qualité, stratégie, date et origine humaine. Un
|
||||||
|
trigger refuse une source devenue non projectable.
|
||||||
|
|
||||||
> **Statut :** architecture courante
|
> **Statut :** architecture courante
|
||||||
> **Version du schéma :** V18
|
> **Version du schéma :** V19
|
||||||
> **Dernière mise à jour :** 2026-07-24
|
> **Dernière mise à jour :** 2026-07-24
|
||||||
> **Source de vérité détaillée :** `SCHEMA_AUDIT_CURRENT.md`
|
> **Source de vérité détaillée :** `SCHEMA_AUDIT_CURRENT.md`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "models/person_evidence_selection.h"
|
#include "models/person_evidence_selection.h"
|
||||||
#include "models/identity_ocr.h"
|
#include "models/identity_ocr.h"
|
||||||
#include "models/identity_traceability.h"
|
#include "models/identity_traceability.h"
|
||||||
|
#include "models/person_ocr_projection.h"
|
||||||
#include <gio/gio.h>
|
#include <gio/gio.h>
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
|
|
@ -33,6 +34,7 @@ typedef enum {
|
||||||
PERSON_CREATION_FAILURE_INSERT_FIELD,
|
PERSON_CREATION_FAILURE_INSERT_FIELD,
|
||||||
PERSON_CREATION_FAILURE_CREATE_SOURCE,
|
PERSON_CREATION_FAILURE_CREATE_SOURCE,
|
||||||
PERSON_CREATION_FAILURE_INSERT_FACTUAL_RELATION,
|
PERSON_CREATION_FAILURE_INSERT_FACTUAL_RELATION,
|
||||||
|
PERSON_CREATION_FAILURE_APPLY_OCR_PROJECTION,
|
||||||
PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,
|
PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,
|
||||||
PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,
|
PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,
|
||||||
PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,
|
PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,
|
||||||
|
|
@ -56,6 +58,8 @@ typedef struct {
|
||||||
gpointer session_check_data;
|
gpointer session_check_data;
|
||||||
/** PersonCreationFactualRelationInput* empruntés, choix humains explicites. */
|
/** PersonCreationFactualRelationInput* empruntés, choix humains explicites. */
|
||||||
const GPtrArray *factual_relations;
|
const GPtrArray *factual_relations;
|
||||||
|
/** PersonOcrFieldProjection* empruntées, toutes choisies explicitement. */
|
||||||
|
const GPtrArray *ocr_projections;
|
||||||
} PersonCreationCoordinatorOptions;
|
} PersonCreationCoordinatorOptions;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char *collected_at;
|
const char *collected_at;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ PersonCreationTaskRequest *person_creation_task_request_new(
|
||||||
const char *database_path, const char *investigation_root_path,
|
const char *database_path, const char *investigation_root_path,
|
||||||
const PersonEntityInput *person,
|
const PersonEntityInput *person,
|
||||||
const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs,
|
const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs,
|
||||||
const GPtrArray *factual_relations);
|
const GPtrArray *factual_relations,const GPtrArray *ocr_projections);
|
||||||
void person_creation_task_request_free(PersonCreationTaskRequest *request);
|
void person_creation_task_request_free(PersonCreationTaskRequest *request);
|
||||||
BackgroundTask *person_creation_task_start(TaskManager *manager,
|
BackgroundTask *person_creation_task_start(TaskManager *manager,
|
||||||
const PersonCreationTaskRequest *request,
|
const PersonCreationTaskRequest *request,
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,6 @@
|
||||||
|
|
||||||
GPtrArray *person_details_provider_get_evidences(Database *database, const char *person_identifier, GError **error);
|
GPtrArray *person_details_provider_get_evidences(Database *database, const char *person_identifier, GError **error);
|
||||||
GPtrArray *person_details_provider_get_factual_relations(Database *database, const char *person_identifier, GError **error);
|
GPtrArray *person_details_provider_get_factual_relations(Database *database, const char *person_identifier, GError **error);
|
||||||
|
GHashTable *person_details_provider_get_profile_fields(Database *database, const char *person_identifier, GError **error);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
9
include/core/person_ocr_projection_mapping.h
Normal file
9
include/core/person_ocr_projection_mapping.h
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#ifndef LABFY_PERSON_OCR_PROJECTION_MAPPING_H
|
||||||
|
#define LABFY_PERSON_OCR_PROJECTION_MAPPING_H
|
||||||
|
#include <glib.h>
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
typedef struct{const char*ocr_code;const char*person_field;const char*label;}PersonOcrProjectionMapping;
|
||||||
|
const PersonOcrProjectionMapping *person_ocr_projection_mapping_for(const char *ocr_code);
|
||||||
|
gboolean person_ocr_projection_mapping_is_compatible(const char *ocr_code,const char *person_field);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
10
include/core/person_ocr_projection_service.h
Normal file
10
include/core/person_ocr_projection_service.h
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#ifndef LABFY_PERSON_OCR_PROJECTION_SERVICE_H
|
||||||
|
#define LABFY_PERSON_OCR_PROJECTION_SERVICE_H
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "models/person_ocr_projection.h"
|
||||||
|
#include <glib.h>
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
GPtrArray *person_ocr_projection_service_candidates(Database *database,const char *evidence_id,const char *run_id,GError **error);
|
||||||
|
gboolean person_ocr_projection_service_apply(Database *database,const char *person_id,const GPtrArray *projections,GError **error);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
15
include/dao/person_ocr_projection_dao.h
Normal file
15
include/dao/person_ocr_projection_dao.h
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#ifndef LABFY_PERSON_OCR_PROJECTION_DAO_H
|
||||||
|
#define LABFY_PERSON_OCR_PROJECTION_DAO_H
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "models/person_ocr_projection.h"
|
||||||
|
#include <glib.h>
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
typedef struct PersonOcrProjectionDao PersonOcrProjectionDao;
|
||||||
|
PersonOcrProjectionDao *person_ocr_projection_dao_new(Database *database);
|
||||||
|
void person_ocr_projection_dao_free(PersonOcrProjectionDao *dao);
|
||||||
|
char *person_ocr_projection_dao_get_value(PersonOcrProjectionDao *dao,const char *person_id,const char *field,GError **error);
|
||||||
|
gboolean person_ocr_projection_dao_apply(PersonOcrProjectionDao *dao,const char *person_id,const PersonOcrFieldProjection *projection,const char *timestamp,GError **error);
|
||||||
|
GPtrArray *person_ocr_projection_dao_list(PersonOcrProjectionDao *dao,const char *person_id,GError **error);
|
||||||
|
GHashTable *person_ocr_projection_dao_list_profile_fields(PersonOcrProjectionDao *dao,const char *person_id,GError **error);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
|
|
@ -99,6 +99,7 @@ bool schema_install_v15(Database *database);
|
||||||
bool schema_install_v16(Database *database);
|
bool schema_install_v16(Database *database);
|
||||||
bool schema_install_v17(Database *database);
|
bool schema_install_v17(Database *database);
|
||||||
bool schema_install_v18(Database *database);
|
bool schema_install_v18(Database *database);
|
||||||
|
bool schema_install_v19(Database *database);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Garantit la présence des extensions du schéma courant V2.
|
* @brief Garantit la présence des extensions du schéma courant V2.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ gboolean identity_field_observation_set_origin(
|
||||||
void identity_field_observation_mark_conflict(IdentityFieldObservation *field);
|
void identity_field_observation_mark_conflict(IdentityFieldObservation *field);
|
||||||
const char *identity_field_observation_get_code(
|
const char *identity_field_observation_get_code(
|
||||||
const IdentityFieldObservation *field);
|
const IdentityFieldObservation *field);
|
||||||
|
const char *identity_field_observation_get_identifier(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
const char *identity_field_observation_get_raw_value(
|
const char *identity_field_observation_get_raw_value(
|
||||||
const IdentityFieldObservation *field);
|
const IdentityFieldObservation *field);
|
||||||
const char *identity_field_observation_get_corrected_value(
|
const char *identity_field_observation_get_corrected_value(
|
||||||
|
|
@ -53,6 +55,8 @@ void identity_field_observation_clear_confirmation(
|
||||||
IdentityFieldObservation *field);
|
IdentityFieldObservation *field);
|
||||||
gboolean identity_field_observation_is_human_confirmed(
|
gboolean identity_field_observation_is_human_confirmed(
|
||||||
const IdentityFieldObservation *field);
|
const IdentityFieldObservation *field);
|
||||||
|
gboolean identity_field_observation_replace_identifier(
|
||||||
|
IdentityFieldObservation *field, const char *identifier);
|
||||||
IdentityReviewStatus identity_field_observation_get_status(
|
IdentityReviewStatus identity_field_observation_get_status(
|
||||||
const IdentityFieldObservation *field);
|
const IdentityFieldObservation *field);
|
||||||
double identity_field_observation_get_confidence(
|
double identity_field_observation_get_confidence(
|
||||||
|
|
|
||||||
28
include/models/person_ocr_projection.h
Normal file
28
include/models/person_ocr_projection.h
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#ifndef LABFY_PERSON_OCR_PROJECTION_H
|
||||||
|
#define LABFY_PERSON_OCR_PROJECTION_H
|
||||||
|
#include <glib.h>
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
typedef enum { PERSON_OCR_KEEP_EXISTING,PERSON_OCR_FILL_EMPTY,PERSON_OCR_REPLACE_EXISTING } PersonOcrProjectionStrategy;
|
||||||
|
typedef struct PersonOcrFieldProjection PersonOcrFieldProjection;
|
||||||
|
PersonOcrFieldProjection *person_ocr_field_projection_new(const char *evidence_id,
|
||||||
|
const char *run_id,const char *ocr_field_id,const char *ocr_code,
|
||||||
|
const char *confirmed_value,const char *quality,const char *review_status,
|
||||||
|
const char *person_field,const char *current_value,
|
||||||
|
PersonOcrProjectionStrategy strategy,gboolean human_confirmed);
|
||||||
|
PersonOcrFieldProjection *person_ocr_field_projection_copy(const PersonOcrFieldProjection *projection);
|
||||||
|
void person_ocr_field_projection_free(PersonOcrFieldProjection *projection);
|
||||||
|
gboolean person_ocr_field_projection_is_valid(const PersonOcrFieldProjection *projection);
|
||||||
|
const char *person_ocr_field_projection_get_evidence_id(const PersonOcrFieldProjection *p);
|
||||||
|
const char *person_ocr_field_projection_get_run_id(const PersonOcrFieldProjection *p);
|
||||||
|
const char *person_ocr_field_projection_get_ocr_field_id(const PersonOcrFieldProjection *p);
|
||||||
|
const char *person_ocr_field_projection_get_ocr_code(const PersonOcrFieldProjection *p);
|
||||||
|
const char *person_ocr_field_projection_get_confirmed_value(const PersonOcrFieldProjection *p);
|
||||||
|
const char *person_ocr_field_projection_get_quality(const PersonOcrFieldProjection *p);
|
||||||
|
const char *person_ocr_field_projection_get_review_status(const PersonOcrFieldProjection *p);
|
||||||
|
const char *person_ocr_field_projection_get_person_field(const PersonOcrFieldProjection *p);
|
||||||
|
const char *person_ocr_field_projection_get_current_value(const PersonOcrFieldProjection *p);
|
||||||
|
PersonOcrProjectionStrategy person_ocr_field_projection_get_strategy(const PersonOcrFieldProjection *p);
|
||||||
|
gboolean person_ocr_field_projection_get_human_confirmed(const PersonOcrFieldProjection *p);
|
||||||
|
gboolean person_ocr_field_projection_replace_evidence_id(PersonOcrFieldProjection *p,const char *evidence_id);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
|
|
@ -46,6 +46,8 @@ const GPtrArray *create_person_dialog_result_get_ocr_runs(
|
||||||
const CreatePersonDialogResult *result);
|
const CreatePersonDialogResult *result);
|
||||||
const GPtrArray *create_person_dialog_result_get_factual_relations(
|
const GPtrArray *create_person_dialog_result_get_factual_relations(
|
||||||
const CreatePersonDialogResult *result);
|
const CreatePersonDialogResult *result);
|
||||||
|
const GPtrArray *create_person_dialog_result_get_ocr_projections(
|
||||||
|
const CreatePersonDialogResult *result);
|
||||||
gboolean create_person_dialog_test_overlay_has_region(GtkWindow *window);
|
gboolean create_person_dialog_test_overlay_has_region(GtkWindow *window);
|
||||||
guint64 create_person_dialog_test_ocr_generation(GtkWindow *window);
|
guint64 create_person_dialog_test_ocr_generation(GtkWindow *window);
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,7 @@ void main_window_set_relation_evidences(
|
||||||
void main_window_set_person_evidences(MainWindow *main_window,
|
void main_window_set_person_evidences(MainWindow *main_window,
|
||||||
const GPtrArray *records);
|
const GPtrArray *records);
|
||||||
void main_window_set_person_factual_relations(MainWindow *main_window,
|
void main_window_set_person_factual_relations(MainWindow *main_window,
|
||||||
const GPtrArray *relations, const GPtrArray *evidence_records);
|
const GPtrArray *relations, const GPtrArray *evidence_records,const GHashTable *profile_fields);
|
||||||
|
|
||||||
|
|
||||||
/** @brief Définit le callback de catégorisation d'une personne. */
|
/** @brief Définit le callback de catégorisation d'une personne. */
|
||||||
|
|
|
||||||
18
include/views/person_creation_confirmation.h
Normal file
18
include/views/person_creation_confirmation.h
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#ifndef LABFY_PERSON_CREATION_CONFIRMATION_H
|
||||||
|
#define LABFY_PERSON_CREATION_CONFIRMATION_H
|
||||||
|
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
|
#include "views/person_factual_relation_editor.h"
|
||||||
|
#include "views/person_ocr_projection_editor.h"
|
||||||
|
#include <glib.h>
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
void person_creation_confirmation_append_sections(
|
||||||
|
GString *summary, const GPtrArray *ocr_runs,
|
||||||
|
PersonOcrProjectionEditor *projection_editor,
|
||||||
|
PersonFactualRelationEditor *relation_editor);
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
|
||||||
|
#endif
|
||||||
16
include/views/person_ocr_projection_editor.h
Normal file
16
include/views/person_ocr_projection_editor.h
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#ifndef LABFY_PERSON_OCR_PROJECTION_EDITOR_H
|
||||||
|
#define LABFY_PERSON_OCR_PROJECTION_EDITOR_H
|
||||||
|
#include "models/identity_ocr.h"
|
||||||
|
#include "models/person_ocr_projection.h"
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
typedef struct PersonOcrProjectionEditor PersonOcrProjectionEditor;
|
||||||
|
PersonOcrProjectionEditor *person_ocr_projection_editor_new(void);
|
||||||
|
GtkWidget *person_ocr_projection_editor_get_widget(PersonOcrProjectionEditor *editor);
|
||||||
|
void person_ocr_projection_editor_set_runs(PersonOcrProjectionEditor *editor,const GPtrArray *runs);
|
||||||
|
gboolean person_ocr_projection_editor_collect(PersonOcrProjectionEditor *editor,GPtrArray **projections,GError **error);
|
||||||
|
void person_ocr_projection_editor_append_summary(
|
||||||
|
PersonOcrProjectionEditor *editor, GString *summary);
|
||||||
|
void person_ocr_projection_editor_free(PersonOcrProjectionEditor *editor);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
|
|
@ -197,6 +197,7 @@ void entity_details_panel_set_person_evidences(
|
||||||
EntityDetailsPanel *details_panel, const GPtrArray *evidence_records);
|
EntityDetailsPanel *details_panel, const GPtrArray *evidence_records);
|
||||||
void entity_details_panel_set_person_factual_relations(
|
void entity_details_panel_set_person_factual_relations(
|
||||||
EntityDetailsPanel *details_panel, const GPtrArray *relations, const GPtrArray *evidence_records);
|
EntityDetailsPanel *details_panel, const GPtrArray *relations, const GPtrArray *evidence_records);
|
||||||
|
void entity_details_panel_set_person_profile_fields(EntityDetailsPanel *details_panel,const GHashTable *fields);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Indique si le volet est actuellement ouvert.
|
* @brief Indique si le volet est actuellement ouvert.
|
||||||
|
|
|
||||||
|
|
@ -474,7 +474,7 @@ void workspace_set_entity_selected_callback(Workspace *workspace,
|
||||||
void workspace_set_person_evidences(Workspace *workspace,
|
void workspace_set_person_evidences(Workspace *workspace,
|
||||||
const GPtrArray *records);
|
const GPtrArray *records);
|
||||||
void workspace_set_person_factual_relations(Workspace *workspace,
|
void workspace_set_person_factual_relations(Workspace *workspace,
|
||||||
const GPtrArray *relations, const GPtrArray *evidence_records);
|
const GPtrArray *relations, const GPtrArray *evidence_records,const GHashTable *profile_fields);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Affiche le graphe chargé avec sa disposition persistée.
|
* @brief Affiche le graphe chargé avec sa disposition persistée.
|
||||||
|
|
|
||||||
|
|
@ -6004,6 +6004,8 @@ static void application_on_person_creation_task_completed(
|
||||||
}
|
}
|
||||||
main_window_set_status(application->main_window,
|
main_window_set_status(application->main_window,
|
||||||
"Personne et preuves ajoutées. Actualisation des vues…");
|
"Personne et preuves ajoutées. Actualisation des vues…");
|
||||||
|
PersonCreationCoordinatorResult *created = background_task_get_result(task);
|
||||||
|
g_free(application->pending_entity_selection_identifier); application->pending_entity_selection_identifier = created != NULL ? g_strdup(created->person_identifier) : NULL;
|
||||||
{
|
{
|
||||||
GError *refresh_error = NULL;
|
GError *refresh_error = NULL;
|
||||||
application_refresh_investigation_tree(
|
application_refresh_investigation_tree(
|
||||||
|
|
@ -6046,7 +6048,8 @@ static void application_on_person_completed(
|
||||||
request = person_creation_task_request_new(active_database, active_root,
|
request = person_creation_task_request_new(active_database, active_root,
|
||||||
input, create_person_dialog_result_get_evidence_selection(result),
|
input, create_person_dialog_result_get_evidence_selection(result),
|
||||||
create_person_dialog_result_get_ocr_runs(result),
|
create_person_dialog_result_get_ocr_runs(result),
|
||||||
create_person_dialog_result_get_factual_relations(result));
|
create_person_dialog_result_get_factual_relations(result),
|
||||||
|
create_person_dialog_result_get_ocr_projections(result));
|
||||||
task_context = g_new0(ApplicationPersonCreationTaskContext, 1);
|
task_context = g_new0(ApplicationPersonCreationTaskContext, 1);
|
||||||
task_context->application = application;
|
task_context->application = application;
|
||||||
task_context->generation = application->session_generation;
|
task_context->generation = application->session_generation;
|
||||||
|
|
@ -7067,12 +7070,14 @@ static void application_on_entity_selected(const char *entity_identifier,
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
GPtrArray *records = person_details_provider_get_evidences(database, entity_identifier, &error);
|
GPtrArray *records = person_details_provider_get_evidences(database, entity_identifier, &error);
|
||||||
GPtrArray *relations = NULL;
|
GPtrArray *relations = NULL;
|
||||||
|
GHashTable *profile_fields = NULL;
|
||||||
if (records != NULL) {
|
if (records != NULL) {
|
||||||
relations = person_details_provider_get_factual_relations(database, entity_identifier, NULL);
|
relations = person_details_provider_get_factual_relations(database, entity_identifier, NULL);
|
||||||
|
profile_fields = person_details_provider_get_profile_fields(database, entity_identifier, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
main_window_set_person_evidences(application->main_window, records);
|
main_window_set_person_evidences(application->main_window, records);
|
||||||
main_window_set_person_factual_relations(application->main_window, relations, records);
|
main_window_set_person_factual_relations(application->main_window, relations, records,profile_fields);
|
||||||
|
|
||||||
if (error != NULL) {
|
if (error != NULL) {
|
||||||
application_present_error(application, "Erreur de chargement", error->message);
|
application_present_error(application, "Erreur de chargement", error->message);
|
||||||
|
|
@ -7080,6 +7085,7 @@ static void application_on_entity_selected(const char *entity_identifier,
|
||||||
}
|
}
|
||||||
if (records != NULL) g_ptr_array_unref(records);
|
if (records != NULL) g_ptr_array_unref(records);
|
||||||
if (relations != NULL) g_ptr_array_unref(relations);
|
if (relations != NULL) g_ptr_array_unref(relations);
|
||||||
|
if (profile_fields != NULL) g_hash_table_unref(profile_fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Contexte possédé par la gestion des preuves d'une personne. */
|
/** @brief Contexte possédé par la gestion des preuves d'une personne. */
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "core/person_creation_coordinator.h"
|
#include "core/person_creation_coordinator.h"
|
||||||
|
#include "core/person_ocr_projection_service.h"
|
||||||
#include "core/file_hash.h"
|
#include "core/file_hash.h"
|
||||||
#include "dao/entity_dao.h"
|
#include "dao/entity_dao.h"
|
||||||
#include "dao/evidence_dao.h"
|
#include "dao/evidence_dao.h"
|
||||||
|
|
@ -477,6 +478,30 @@ static PersonCreationCoordinatorResult *person_creation_coordinator_execute_inte
|
||||||
options != NULL ? options->factual_relations : NULL,
|
options != NULL ? options->factual_relations : NULL,
|
||||||
timestamp, &failure, error))
|
timestamp, &failure, error))
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
|
if (options != NULL && options->ocr_projections != NULL) {
|
||||||
|
GPtrArray *projections = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) person_ocr_field_projection_free);
|
||||||
|
for (guint i = 0; i < options->ocr_projections->len; i++) {
|
||||||
|
PersonOcrFieldProjection *projection =
|
||||||
|
person_ocr_field_projection_copy(g_ptr_array_index(
|
||||||
|
(GPtrArray *) options->ocr_projections, i));
|
||||||
|
for (guint j = 0; projection != NULL && ocr_runs != NULL &&
|
||||||
|
j < ocr_runs->len; j++) {
|
||||||
|
IdentityOcrRun *run = g_ptr_array_index((GPtrArray *) ocr_runs, j);
|
||||||
|
if (g_strcmp0(identity_ocr_run_get_identifier(run),
|
||||||
|
person_ocr_field_projection_get_run_id(projection)) == 0)
|
||||||
|
person_ocr_field_projection_replace_evidence_id(projection,
|
||||||
|
identity_ocr_run_get_evidence_id(run));
|
||||||
|
}
|
||||||
|
if (projection != NULL) g_ptr_array_add(projections, projection);
|
||||||
|
}
|
||||||
|
gboolean projected = !fail_at(&failure,
|
||||||
|
PERSON_CREATION_FAILURE_APPLY_OCR_PROJECTION, error) &&
|
||||||
|
person_ocr_projection_service_apply(database,
|
||||||
|
result->person_identifier, projections, error);
|
||||||
|
g_ptr_array_unref(projections);
|
||||||
|
if (!projected) goto cleanup;
|
||||||
|
}
|
||||||
for (guint i = 0; ocr_runs != NULL && i < ocr_runs->len; i++) {
|
for (guint i = 0; ocr_runs != NULL && i < ocr_runs->len; i++) {
|
||||||
IdentityOcrRun *run = g_ptr_array_index((GPtrArray *) ocr_runs, i);
|
IdentityOcrRun *run = g_ptr_array_index((GPtrArray *) ocr_runs, i);
|
||||||
char *directory = g_build_filename(root, "02_Preuves_Traitees",
|
char *directory = g_build_filename(root, "02_Preuves_Traitees",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "core/person_creation_task.h"
|
#include "core/person_creation_task.h"
|
||||||
|
#include "models/person_ocr_projection.h"
|
||||||
|
|
||||||
struct PersonCreationTaskRequest {
|
struct PersonCreationTaskRequest {
|
||||||
char *database_path;
|
char *database_path;
|
||||||
|
|
@ -13,12 +14,13 @@ struct PersonCreationTaskRequest {
|
||||||
PersonEvidenceSelection *selection;
|
PersonEvidenceSelection *selection;
|
||||||
GPtrArray *ocr_runs;
|
GPtrArray *ocr_runs;
|
||||||
GPtrArray *factual_relations;
|
GPtrArray *factual_relations;
|
||||||
|
GPtrArray *ocr_projections;
|
||||||
};
|
};
|
||||||
PersonCreationTaskRequest *person_creation_task_request_new(
|
PersonCreationTaskRequest *person_creation_task_request_new(
|
||||||
const char *database_path, const char *root,
|
const char *database_path, const char *root,
|
||||||
const PersonEntityInput *person,
|
const PersonEntityInput *person,
|
||||||
const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs,
|
const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs,
|
||||||
const GPtrArray *factual_relations)
|
const GPtrArray *factual_relations,const GPtrArray *ocr_projections)
|
||||||
{
|
{
|
||||||
PersonCreationTaskRequest *request;
|
PersonCreationTaskRequest *request;
|
||||||
if (database_path == NULL || root == NULL || person == NULL) return NULL;
|
if (database_path == NULL || root == NULL || person == NULL) return NULL;
|
||||||
|
|
@ -65,6 +67,11 @@ PersonCreationTaskRequest *person_creation_task_request_new(
|
||||||
g_ptr_array_add(request->factual_relations, copy_rel);
|
g_ptr_array_add(request->factual_relations, copy_rel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
request->ocr_projections=g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)person_ocr_field_projection_free);
|
||||||
|
for(guint i=0;ocr_projections&&i<ocr_projections->len;i++)
|
||||||
|
g_ptr_array_add(request->ocr_projections,person_ocr_field_projection_copy(
|
||||||
|
g_ptr_array_index((GPtrArray*)ocr_projections,i)));
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +94,7 @@ void person_creation_task_request_free(PersonCreationTaskRequest *request)
|
||||||
}
|
}
|
||||||
g_ptr_array_unref(request->factual_relations);
|
g_ptr_array_unref(request->factual_relations);
|
||||||
}
|
}
|
||||||
|
g_ptr_array_unref(request->ocr_projections);
|
||||||
g_free(request);
|
g_free(request);
|
||||||
}
|
}
|
||||||
static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
|
static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
|
||||||
|
|
@ -102,9 +110,10 @@ static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
|
||||||
"Impossible d’ouvrir la base de l’enquête.");
|
"Impossible d’ouvrir la base de l’enquête.");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
if (request->factual_relations != NULL && request->factual_relations->len > 0) {
|
if ((request->factual_relations != NULL && request->factual_relations->len > 0)||request->ocr_projections->len>0) {
|
||||||
PersonCreationCoordinatorOptions options = {0};
|
PersonCreationCoordinatorOptions options = {0};
|
||||||
options.factual_relations = request->factual_relations;
|
options.factual_relations = request->factual_relations;
|
||||||
|
options.ocr_projections = request->ocr_projections;
|
||||||
*result = person_creation_coordinator_execute_with_options(database, request->root,
|
*result = person_creation_coordinator_execute_with_options(database, request->root,
|
||||||
&request->person, request->selection, request->ocr_runs, &options,
|
&request->person, request->selection, request->ocr_runs, &options,
|
||||||
cancellable, error);
|
cancellable, error);
|
||||||
|
|
@ -127,7 +136,7 @@ BackgroundTask *person_creation_task_start(TaskManager *manager,
|
||||||
task = background_task_new("Création de la personne et import des preuves");
|
task = background_task_new("Création de la personne et import des preuves");
|
||||||
copy = person_creation_task_request_new(request->database_path,
|
copy = person_creation_task_request_new(request->database_path,
|
||||||
request->root, &request->person, request->selection,
|
request->root, &request->person, request->selection,
|
||||||
request->ocr_runs, request->factual_relations);
|
request->ocr_runs, request->factual_relations,request->ocr_projections);
|
||||||
if (task == NULL || copy == NULL ||
|
if (task == NULL || copy == NULL ||
|
||||||
!task_manager_add(manager, task, error) ||
|
!task_manager_add(manager, task, error) ||
|
||||||
!background_task_start(task, worker, copy,
|
!background_task_start(task, worker, copy,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include "dao/evidence_dao.h"
|
#include "dao/evidence_dao.h"
|
||||||
#include "dao/evidence_entity_dao.h"
|
#include "dao/evidence_entity_dao.h"
|
||||||
#include "dao/identity_traceability_dao.h"
|
#include "dao/identity_traceability_dao.h"
|
||||||
|
#include "dao/person_ocr_projection_dao.h"
|
||||||
#include "models/evidence_record.h"
|
#include "models/evidence_record.h"
|
||||||
|
|
||||||
GPtrArray *person_details_provider_get_evidences(Database *database, const char *person_identifier, GError **error)
|
GPtrArray *person_details_provider_get_evidences(Database *database, const char *person_identifier, GError **error)
|
||||||
|
|
@ -43,3 +44,14 @@ GPtrArray *person_details_provider_get_factual_relations(Database *database, con
|
||||||
identity_traceability_dao_free(trace_dao);
|
identity_traceability_dao_free(trace_dao);
|
||||||
return relations;
|
return relations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GHashTable *person_details_provider_get_profile_fields(Database *database,
|
||||||
|
const char *person_identifier, GError **error)
|
||||||
|
{
|
||||||
|
PersonOcrProjectionDao *dao = person_ocr_projection_dao_new(database);
|
||||||
|
if (dao == NULL) return NULL;
|
||||||
|
GHashTable *fields = person_ocr_projection_dao_list_profile_fields(
|
||||||
|
dao, person_identifier, error);
|
||||||
|
person_ocr_projection_dao_free(dao);
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
|
||||||
22
src/core/person_ocr_projection_mapping.c
Normal file
22
src/core/person_ocr_projection_mapping.c
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#include "core/person_ocr_projection_mapping.h"
|
||||||
|
static const PersonOcrProjectionMapping mappings[] = {
|
||||||
|
{"surname", "surname", "Nom déclaré"},
|
||||||
|
{"birth_name", "surname", "Nom déclaré"},
|
||||||
|
{"given_names", "given_names", "Prénoms déclarés"},
|
||||||
|
{"birth_date", "birth_date", "Date de naissance déclarée"},
|
||||||
|
{"birth_place", "birth_place", "Lieu de naissance déclaré"},
|
||||||
|
{"nationality", "nationality", "Nationalité déclarée"},
|
||||||
|
{"sex_as_printed", "sex_as_printed", "Sexe imprimé"},
|
||||||
|
{"address_as_printed", "address_as_printed", "Adresse imprimée"}};
|
||||||
|
const PersonOcrProjectionMapping *
|
||||||
|
person_ocr_projection_mapping_for(const char *c) {
|
||||||
|
for (guint i = 0; i < G_N_ELEMENTS(mappings); i++)
|
||||||
|
if (g_strcmp0(c, mappings[i].ocr_code) == 0)
|
||||||
|
return &mappings[i];
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
gboolean person_ocr_projection_mapping_is_compatible(const char *c,
|
||||||
|
const char *t) {
|
||||||
|
const PersonOcrProjectionMapping *m = person_ocr_projection_mapping_for(c);
|
||||||
|
return m && g_strcmp0(m->person_field, t) == 0;
|
||||||
|
}
|
||||||
154
src/core/person_ocr_projection_service.c
Normal file
154
src/core/person_ocr_projection_service.c
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
#include "core/person_ocr_projection_service.h"
|
||||||
|
#include "core/person_ocr_projection_mapping.h"
|
||||||
|
#include "dao/entity_dao.h"
|
||||||
|
#include "dao/identity_ocr_dao.h"
|
||||||
|
#include "dao/person_ocr_projection_dao.h"
|
||||||
|
#include "database/transaction.h"
|
||||||
|
#include "models/identity_traceability.h"
|
||||||
|
static GQuark domain(void) {
|
||||||
|
return g_quark_from_static_string("person-ocr-projection-service");
|
||||||
|
}
|
||||||
|
GPtrArray *person_ocr_projection_service_candidates(Database *d,
|
||||||
|
const char *evidence,
|
||||||
|
const char *run,
|
||||||
|
GError **error) {
|
||||||
|
if (!d || !g_uuid_string_is_valid(evidence) ||
|
||||||
|
!g_uuid_string_is_valid(run)) {
|
||||||
|
g_set_error_literal(error, domain(), 1,
|
||||||
|
"La preuve ou l’exécution OCR est invalide.");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
IdentityOcrDao *dao = identity_ocr_dao_new(d);
|
||||||
|
IdentityOcrRunRecord *r =
|
||||||
|
dao ? identity_ocr_dao_find_run(dao, run, error) : NULL;
|
||||||
|
if (!r || g_strcmp0(r->evidence_id, evidence) != 0) {
|
||||||
|
if (error && !*error)
|
||||||
|
g_set_error_literal(error, domain(), 1,
|
||||||
|
"L’exécution OCR ne correspond pas à la preuve.");
|
||||||
|
identity_ocr_run_record_free(r);
|
||||||
|
identity_ocr_dao_free(dao);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
GPtrArray *docs = identity_ocr_dao_list_documents_by_evidence(dao, evidence,
|
||||||
|
error),
|
||||||
|
*out = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)identity_field_observation_record_free);
|
||||||
|
if (docs == NULL) {
|
||||||
|
g_ptr_array_unref(out);
|
||||||
|
identity_ocr_run_record_free(r);
|
||||||
|
identity_ocr_dao_free(dao);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
for (guint i = 0; docs && i < docs->len; i++) {
|
||||||
|
IdentityDocumentObservationRecord *doc = g_ptr_array_index(docs, i);
|
||||||
|
if (g_strcmp0(doc->ocr_run_id, run))
|
||||||
|
continue;
|
||||||
|
GPtrArray *f = identity_ocr_dao_list_confirmed_fields(dao, doc->id, error);
|
||||||
|
if (f == NULL) {
|
||||||
|
g_ptr_array_unref(out);
|
||||||
|
g_ptr_array_unref(docs);
|
||||||
|
identity_ocr_run_record_free(r);
|
||||||
|
identity_ocr_dao_free(dao);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
for (guint j = 0; f && j < f->len; j++) {
|
||||||
|
IdentityFieldObservationRecord *v = g_ptr_array_index(f, j);
|
||||||
|
if (person_ocr_projection_mapping_for(v->field_code))
|
||||||
|
g_ptr_array_add(out, g_ptr_array_steal_index(f, j--));
|
||||||
|
}
|
||||||
|
g_clear_pointer(&f, g_ptr_array_unref);
|
||||||
|
}
|
||||||
|
g_clear_pointer(&docs, g_ptr_array_unref);
|
||||||
|
identity_ocr_run_record_free(r);
|
||||||
|
identity_ocr_dao_free(dao);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
gboolean person_ocr_projection_service_apply(Database *d, const char *person,
|
||||||
|
const GPtrArray *items,
|
||||||
|
GError **error) {
|
||||||
|
if (!d || !g_uuid_string_is_valid(person) || !items) {
|
||||||
|
g_set_error_literal(error, domain(), 2, "Projection invalide.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
EntityDao *entities = entity_dao_new(d, error);
|
||||||
|
EntityRecord *entity =
|
||||||
|
entities ? entity_dao_find_by_identifier(entities, person, error) : NULL;
|
||||||
|
if (!entity ||
|
||||||
|
g_strcmp0(entity_record_get_type_identifier(entity), "person")) {
|
||||||
|
if (error && !*error)
|
||||||
|
g_set_error_literal(error, domain(), 2,
|
||||||
|
"La personne cible est invalide.");
|
||||||
|
entity_record_free(entity);
|
||||||
|
entity_dao_free(entities);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
entity_record_free(entity);
|
||||||
|
entity_dao_free(entities);
|
||||||
|
PersonOcrProjectionDao *dao = person_ocr_projection_dao_new(d);
|
||||||
|
IdentityOcrDao *ocr = identity_ocr_dao_new(d);
|
||||||
|
gboolean owns = !database_transaction_is_active(d);
|
||||||
|
if (owns && !database_transaction_begin(d))
|
||||||
|
goto fail;
|
||||||
|
GDateTime *now = g_date_time_new_now_utc();
|
||||||
|
char *at = g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ");
|
||||||
|
for (guint i = 0; i < items->len; i++) {
|
||||||
|
PersonOcrFieldProjection *p = g_ptr_array_index((GPtrArray *)items, i);
|
||||||
|
if (person_ocr_field_projection_get_strategy(p) == PERSON_OCR_KEEP_EXISTING)
|
||||||
|
continue;
|
||||||
|
IdentityFieldObservationRecord *f = identity_ocr_dao_find_field(
|
||||||
|
ocr, person_ocr_field_projection_get_ocr_field_id(p), error);
|
||||||
|
char *current = person_ocr_projection_dao_get_value(
|
||||||
|
dao, person, person_ocr_field_projection_get_person_field(p), error);
|
||||||
|
if (error && *error) {
|
||||||
|
identity_field_observation_record_free(f);
|
||||||
|
g_free(current);
|
||||||
|
g_free(at);
|
||||||
|
g_date_time_unref(now);
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
gboolean valid =
|
||||||
|
f &&
|
||||||
|
identity_traceability_field_is_projectable(
|
||||||
|
f->review_status, f->value_quality, f->confirmation_state,
|
||||||
|
f->confirmed_value) &&
|
||||||
|
g_strcmp0(f->evidence_id,
|
||||||
|
person_ocr_field_projection_get_evidence_id(p)) == 0 &&
|
||||||
|
g_strcmp0(f->ocr_run_id, person_ocr_field_projection_get_run_id(p)) ==
|
||||||
|
0 &&
|
||||||
|
g_strcmp0(f->field_code, person_ocr_field_projection_get_ocr_code(p)) ==
|
||||||
|
0 &&
|
||||||
|
g_strcmp0(f->confirmed_value,
|
||||||
|
person_ocr_field_projection_get_confirmed_value(p)) == 0 &&
|
||||||
|
person_ocr_projection_mapping_is_compatible(
|
||||||
|
f->field_code, person_ocr_field_projection_get_person_field(p)) &&
|
||||||
|
g_strcmp0(current, person_ocr_field_projection_get_current_value(p)) ==
|
||||||
|
0 &&
|
||||||
|
((!current && person_ocr_field_projection_get_strategy(p) ==
|
||||||
|
PERSON_OCR_FILL_EMPTY) ||
|
||||||
|
(current && person_ocr_field_projection_get_strategy(p) ==
|
||||||
|
PERSON_OCR_REPLACE_EXISTING));
|
||||||
|
identity_field_observation_record_free(f);
|
||||||
|
g_free(current);
|
||||||
|
if (!valid || !person_ocr_projection_dao_apply(dao, person, p, at, error)) {
|
||||||
|
g_free(at);
|
||||||
|
g_date_time_unref(now);
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_free(at);
|
||||||
|
g_date_time_unref(now);
|
||||||
|
if (owns && !database_transaction_commit(d))
|
||||||
|
goto fail;
|
||||||
|
person_ocr_projection_dao_free(dao);
|
||||||
|
identity_ocr_dao_free(ocr);
|
||||||
|
return TRUE;
|
||||||
|
fail:
|
||||||
|
if (owns && database_transaction_is_active(d))
|
||||||
|
database_transaction_rollback(d);
|
||||||
|
if (error && !*error)
|
||||||
|
g_set_error_literal(error, domain(), 3,
|
||||||
|
"La valeur OCR a changé ou n’est plus projetable.");
|
||||||
|
person_ocr_projection_dao_free(dao);
|
||||||
|
identity_ocr_dao_free(ocr);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
@ -64,7 +64,7 @@ gboolean identity_ocr_dao_insert(IdentityOcrDao*d,const char*person,
|
||||||
const GPtrArray*fields=identity_ocr_run_get_fields(r);
|
const GPtrArray*fields=identity_ocr_run_get_fields(r);
|
||||||
for(guint i=0;fields&&i<fields->len;i++){IdentityFieldObservation*f=g_ptr_array_index((GPtrArray*)fields,i);
|
for(guint i=0;fields&&i<fields->len;i++){IdentityFieldObservation*f=g_ptr_array_index((GPtrArray*)fields,i);
|
||||||
IdentityReviewStatus status=identity_field_observation_get_status(f);
|
IdentityReviewStatus status=identity_field_observation_get_status(f);
|
||||||
const IdentitySourceBox*b=identity_field_observation_get_box(f);char*id=g_uuid_string_random();
|
const IdentitySourceBox*b=identity_field_observation_get_box(f);const char*id=identity_field_observation_get_identifier(f);
|
||||||
s=database_statement_prepare(d->database,"INSERT INTO identity_field_observations("
|
s=database_statement_prepare(d->database,"INSERT INTO identity_field_observations("
|
||||||
"id,observation_id,field_code,raw_value,corrected_value,normalized_value,"
|
"id,observation_id,field_code,raw_value,corrected_value,normalized_value,"
|
||||||
"confidence,review_status,"
|
"confidence,review_status,"
|
||||||
|
|
@ -92,7 +92,7 @@ gboolean identity_ocr_dao_insert(IdentityOcrDao*d,const char*person,
|
||||||
?"human_confirmed":"unconfirmed")&&
|
?"human_confirmed":"unconfirmed")&&
|
||||||
bind_text(s,23,identity_field_observation_get_value_quality(f))&&
|
bind_text(s,23,identity_field_observation_get_value_quality(f))&&
|
||||||
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
database_statement_finalize(s);g_free(id);if(!ok){g_free(obs);goto fail;}}
|
database_statement_finalize(s);if(!ok){g_free(obs);goto fail;}}
|
||||||
g_free(obs);return TRUE;
|
g_free(obs);return TRUE;
|
||||||
fail:g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),1,
|
fail:g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),1,
|
||||||
"Impossible de conserver l’OCR d’identité.");return FALSE;
|
"Impossible de conserver l’OCR d’identité.");return FALSE;
|
||||||
|
|
@ -357,6 +357,7 @@ IdentityOcrRun *identity_ocr_dao_load_run(
|
||||||
:identity_field_observation_new_manual(f->field_code,
|
:identity_field_observation_new_manual(f->field_code,
|
||||||
f->corrected_value,(guint)f->display_order);
|
f->corrected_value,(guint)f->display_order);
|
||||||
if(field==NULL)continue;
|
if(field==NULL)continue;
|
||||||
|
identity_field_observation_replace_identifier(field,f->id);
|
||||||
if(f->corrected_value!=NULL)
|
if(f->corrected_value!=NULL)
|
||||||
identity_field_observation_modify(field,f->corrected_value,f->review_note);
|
identity_field_observation_modify(field,f->corrected_value,f->review_note);
|
||||||
if(g_strcmp0(f->review_status,"accepted")==0)
|
if(g_strcmp0(f->review_status,"accepted")==0)
|
||||||
|
|
@ -393,7 +394,7 @@ static gboolean update_review_fields(IdentityOcrDao*d,const char*observation,
|
||||||
for(guint i=0;fields!=NULL&&i<fields->len;i++){
|
for(guint i=0;fields!=NULL&&i<fields->len;i++){
|
||||||
IdentityFieldObservation*f=g_ptr_array_index((GPtrArray*)fields,i);
|
IdentityFieldObservation*f=g_ptr_array_index((GPtrArray*)fields,i);
|
||||||
const IdentitySourceBox*b=identity_field_observation_get_box(f);
|
const IdentitySourceBox*b=identity_field_observation_get_box(f);
|
||||||
char*id=g_uuid_string_random();
|
const char*id=identity_field_observation_get_identifier(f);
|
||||||
DatabaseStatement*s=database_statement_prepare(d->database,
|
DatabaseStatement*s=database_statement_prepare(d->database,
|
||||||
"INSERT INTO identity_field_observations(id,observation_id,field_code,"
|
"INSERT INTO identity_field_observations(id,observation_id,field_code,"
|
||||||
"raw_value,corrected_value,normalized_value,confidence,review_status,origin,evidence_id,"
|
"raw_value,corrected_value,normalized_value,confidence,review_status,origin,evidence_id,"
|
||||||
|
|
@ -431,7 +432,7 @@ static gboolean update_review_fields(IdentityOcrDao*d,const char*observation,
|
||||||
?"human_confirmed":"unconfirmed")&&
|
?"human_confirmed":"unconfirmed")&&
|
||||||
bind_text(s,23,identity_field_observation_get_value_quality(f))&&
|
bind_text(s,23,identity_field_observation_get_value_quality(f))&&
|
||||||
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
database_statement_finalize(s);g_free(id);if(!ok)return FALSE;
|
database_statement_finalize(s);if(!ok)return FALSE;
|
||||||
}
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
168
src/dao/person_ocr_projection_dao.c
Normal file
168
src/dao/person_ocr_projection_dao.c
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
#include "dao/person_ocr_projection_dao.h"
|
||||||
|
#include "database/statement.h"
|
||||||
|
struct PersonOcrProjectionDao {
|
||||||
|
Database *database;
|
||||||
|
};
|
||||||
|
static GQuark domain(void) {
|
||||||
|
return g_quark_from_static_string("person-ocr-projection-dao");
|
||||||
|
}
|
||||||
|
PersonOcrProjectionDao *person_ocr_projection_dao_new(Database *d) {
|
||||||
|
if (!d)
|
||||||
|
return NULL;
|
||||||
|
PersonOcrProjectionDao *x = g_new0(PersonOcrProjectionDao, 1);
|
||||||
|
x->database = d;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
void person_ocr_projection_dao_free(PersonOcrProjectionDao *d) { g_free(d); }
|
||||||
|
char *person_ocr_projection_dao_get_value(PersonOcrProjectionDao *d,
|
||||||
|
const char *p, const char *f,
|
||||||
|
GError **e) {
|
||||||
|
if (!d || !p || !f)
|
||||||
|
return NULL;
|
||||||
|
DatabaseStatement *s = database_statement_prepare(
|
||||||
|
d->database, "SELECT value FROM person_profile_fields WHERE person_id=? "
|
||||||
|
"AND field_code=?;");
|
||||||
|
char *v = NULL;
|
||||||
|
if (!s || !database_statement_bind_text(s, 1, p) ||
|
||||||
|
!database_statement_bind_text(s, 2, f))
|
||||||
|
goto fail;
|
||||||
|
DatabaseStatementStepResult step = database_statement_step(s);
|
||||||
|
if (step == DATABASE_STATEMENT_STEP_ROW)
|
||||||
|
database_statement_column_text(s, 0, &v);
|
||||||
|
else if (step != DATABASE_STATEMENT_STEP_DONE)
|
||||||
|
goto fail;
|
||||||
|
database_statement_finalize(s);
|
||||||
|
return v;
|
||||||
|
fail:
|
||||||
|
database_statement_finalize(s);
|
||||||
|
g_set_error_literal(e, domain(), 1,
|
||||||
|
"Impossible de lire le champ de la personne.");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
gboolean person_ocr_projection_dao_apply(PersonOcrProjectionDao *d,
|
||||||
|
const char *person,
|
||||||
|
const PersonOcrFieldProjection *p,
|
||||||
|
const char *at, GError **e) {
|
||||||
|
if (!d || !person_ocr_field_projection_is_valid(p) || !at)
|
||||||
|
return FALSE;
|
||||||
|
const char *target = person_ocr_field_projection_get_person_field(p),
|
||||||
|
*value = person_ocr_field_projection_get_confirmed_value(p);
|
||||||
|
DatabaseStatement *s = database_statement_prepare(
|
||||||
|
d->database,
|
||||||
|
"INSERT INTO "
|
||||||
|
"person_profile_fields(person_id,field_code,value,updated_at) "
|
||||||
|
"VALUES(?,?,?,?) ON CONFLICT(person_id,field_code) DO UPDATE SET "
|
||||||
|
"value=excluded.value,updated_at=excluded.updated_at;");
|
||||||
|
gboolean ok = s && database_statement_bind_text(s, 1, person) &&
|
||||||
|
database_statement_bind_text(s, 2, target) &&
|
||||||
|
database_statement_bind_text(s, 3, value) &&
|
||||||
|
database_statement_bind_text(s, 4, at) &&
|
||||||
|
database_statement_step(s) == DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
database_statement_finalize(s);
|
||||||
|
if (!ok)
|
||||||
|
goto fail;
|
||||||
|
char *id = g_uuid_string_random();
|
||||||
|
s = database_statement_prepare(d->database,
|
||||||
|
"INSERT INTO person_ocr_field_projections "
|
||||||
|
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
|
||||||
|
const char *previous = person_ocr_field_projection_get_current_value(p);
|
||||||
|
const char *strategy =
|
||||||
|
person_ocr_field_projection_get_strategy(p) == PERSON_OCR_REPLACE_EXISTING
|
||||||
|
? "replace_existing"
|
||||||
|
: "fill_empty";
|
||||||
|
ok = s && database_statement_bind_text(s, 1, id) &&
|
||||||
|
database_statement_bind_text(s, 2, person) &&
|
||||||
|
database_statement_bind_text(s, 3, target) &&
|
||||||
|
(previous ? database_statement_bind_text(s, 4, previous)
|
||||||
|
: database_statement_bind_null(s, 4)) &&
|
||||||
|
database_statement_bind_text(s, 5, value) &&
|
||||||
|
database_statement_bind_text(
|
||||||
|
s, 6, person_ocr_field_projection_get_evidence_id(p)) &&
|
||||||
|
database_statement_bind_text(
|
||||||
|
s, 7, person_ocr_field_projection_get_run_id(p)) &&
|
||||||
|
database_statement_bind_text(
|
||||||
|
s, 8, person_ocr_field_projection_get_ocr_field_id(p)) &&
|
||||||
|
database_statement_bind_text(
|
||||||
|
s, 9, person_ocr_field_projection_get_ocr_code(p)) &&
|
||||||
|
database_statement_bind_text(
|
||||||
|
s, 10, person_ocr_field_projection_get_quality(p)) &&
|
||||||
|
database_statement_bind_text(
|
||||||
|
s, 11, person_ocr_field_projection_get_review_status(p)) &&
|
||||||
|
database_statement_bind_text(s, 12, strategy) &&
|
||||||
|
database_statement_bind_text(s, 13, at) &&
|
||||||
|
database_statement_bind_text(s, 14, "human") &&
|
||||||
|
database_statement_step(s) == DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
database_statement_finalize(s);
|
||||||
|
g_free(id);
|
||||||
|
if (ok)
|
||||||
|
return TRUE;
|
||||||
|
fail:
|
||||||
|
g_set_error_literal(e, domain(), 2,
|
||||||
|
"Impossible de conserver la projection OCR.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
GPtrArray *person_ocr_projection_dao_list(PersonOcrProjectionDao *d,
|
||||||
|
const char *p, GError **e) {
|
||||||
|
GPtrArray *a = g_ptr_array_new_with_free_func(g_free);
|
||||||
|
DatabaseStatement *s =
|
||||||
|
d ? database_statement_prepare(
|
||||||
|
d->database, "SELECT id FROM person_ocr_field_projections WHERE "
|
||||||
|
"person_id=? ORDER BY projected_at,id;")
|
||||||
|
: NULL;
|
||||||
|
if (!s || !database_statement_bind_text(s, 1, p))
|
||||||
|
goto fail;
|
||||||
|
for (;;) {
|
||||||
|
DatabaseStatementStepResult step = database_statement_step(s);
|
||||||
|
if (step == DATABASE_STATEMENT_STEP_DONE)
|
||||||
|
break;
|
||||||
|
if (step != DATABASE_STATEMENT_STEP_ROW)
|
||||||
|
goto fail;
|
||||||
|
char *id = NULL;
|
||||||
|
if (!database_statement_column_text(s, 0, &id))
|
||||||
|
goto fail;
|
||||||
|
g_ptr_array_add(a, id);
|
||||||
|
}
|
||||||
|
database_statement_finalize(s);
|
||||||
|
return a;
|
||||||
|
fail:
|
||||||
|
database_statement_finalize(s);
|
||||||
|
g_ptr_array_unref(a);
|
||||||
|
g_set_error_literal(e, domain(), 3,
|
||||||
|
"Impossible de lire les projections OCR.");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
GHashTable *person_ocr_projection_dao_list_profile_fields(
|
||||||
|
PersonOcrProjectionDao *d, const char *person, GError **e) {
|
||||||
|
GHashTable *fields = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
|
||||||
|
g_free);
|
||||||
|
DatabaseStatement *s = d ? database_statement_prepare(
|
||||||
|
d->database,
|
||||||
|
"SELECT field_code,value FROM "
|
||||||
|
"person_profile_fields WHERE person_id=? "
|
||||||
|
"ORDER BY field_code;")
|
||||||
|
: NULL;
|
||||||
|
if (!s || !database_statement_bind_text(s, 1, person))
|
||||||
|
goto fail;
|
||||||
|
for (;;) {
|
||||||
|
DatabaseStatementStepResult step = database_statement_step(s);
|
||||||
|
if (step == DATABASE_STATEMENT_STEP_DONE)
|
||||||
|
break;
|
||||||
|
char *field = NULL, *value = NULL;
|
||||||
|
if (step != DATABASE_STATEMENT_STEP_ROW ||
|
||||||
|
!database_statement_column_text(s, 0, &field) ||
|
||||||
|
!database_statement_column_text(s, 1, &value)) {
|
||||||
|
g_free(field);
|
||||||
|
g_free(value);
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
g_hash_table_insert(fields, field, value);
|
||||||
|
}
|
||||||
|
database_statement_finalize(s);
|
||||||
|
return fields;
|
||||||
|
fail:
|
||||||
|
database_statement_finalize(s);
|
||||||
|
g_hash_table_unref(fields);
|
||||||
|
g_set_error_literal(e, domain(), 4,
|
||||||
|
"Impossible de lire le profil structuré.");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
@ -16,12 +16,12 @@
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle du schéma SQLite.
|
* @brief Version actuelle du schéma SQLite.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT 18
|
#define DATABASE_SCHEMA_VERSION_CURRENT 19
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "18"
|
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "19"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||||
|
|
@ -932,6 +932,21 @@ rollback:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool database_migrate_v18_to_v19(Database *database)
|
||||||
|
{
|
||||||
|
bool transaction_started = false;
|
||||||
|
if (database == NULL || !database_transaction_begin(database)) return false;
|
||||||
|
transaction_started = true;
|
||||||
|
if (!schema_install_v19(database) ||
|
||||||
|
!database_update_schema_version(database, "19") ||
|
||||||
|
!database_transaction_commit(database)) goto rollback;
|
||||||
|
return true;
|
||||||
|
rollback:
|
||||||
|
if (transaction_started && !database_transaction_rollback(database))
|
||||||
|
g_warning("Impossible d’annuler la migration SQLite V18 vers V19.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1193,6 +1208,10 @@ bool database_migrate_to_latest(
|
||||||
if (!database_migrate_v17_to_v18(database)) return false;
|
if (!database_migrate_v17_to_v18(database)) return false;
|
||||||
schema_version = 18;
|
schema_version = 18;
|
||||||
break;
|
break;
|
||||||
|
case 18:
|
||||||
|
if (!database_migrate_v18_to_v19(database)) return false;
|
||||||
|
schema_version = 19;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
database_set_error(
|
database_set_error(
|
||||||
|
|
@ -1335,7 +1354,8 @@ bool database_initialize(
|
||||||
|
|
||||||
if (!schema_ensure_current(
|
if (!schema_ensure_current(
|
||||||
database
|
database
|
||||||
))
|
) ||
|
||||||
|
!schema_install_v19(database))
|
||||||
{
|
{
|
||||||
goto rollback;
|
goto rollback;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -433,6 +433,12 @@ bool schema_install_v18(Database *database)
|
||||||
"la migration SQLite V18");
|
"la migration SQLite V18");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool schema_install_v19(Database *database)
|
||||||
|
{
|
||||||
|
return schema_execute_file(database, "database/schema_v19.sql",
|
||||||
|
"la migration SQLite V19");
|
||||||
|
}
|
||||||
|
|
||||||
bool schema_ensure_current(
|
bool schema_ensure_current(
|
||||||
Database *database
|
Database *database
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#include "models/identity_ocr.h"
|
#include "models/identity_ocr.h"
|
||||||
struct IdentityFieldObservation {
|
struct IdentityFieldObservation {
|
||||||
char *code, *raw_value, *corrected_value, *normalized_value, *note;
|
char *id, *code, *raw_value, *corrected_value, *normalized_value, *note;
|
||||||
char *confirmed_value, *value_quality;
|
char *confirmed_value, *value_quality;
|
||||||
char *origin;
|
char *origin;
|
||||||
double confidence;
|
double confidence;
|
||||||
|
|
@ -47,7 +47,7 @@ IdentityFieldObservation *identity_field_observation_new(
|
||||||
if (!identity_ocr_field_code_is_valid(code) || raw == NULL ||
|
if (!identity_ocr_field_code_is_valid(code) || raw == NULL ||
|
||||||
raw[0]=='\0' || confidence < -1.0 || confidence > 100.0) return NULL;
|
raw[0]=='\0' || confidence < -1.0 || confidence > 100.0) return NULL;
|
||||||
IdentityFieldObservation *f=g_new0(IdentityFieldObservation,1);
|
IdentityFieldObservation *f=g_new0(IdentityFieldObservation,1);
|
||||||
f->code=g_strdup(code); f->raw_value=g_strdup(raw);
|
f->id=g_uuid_string_random();f->code=g_strdup(code); f->raw_value=g_strdup(raw);
|
||||||
f->confidence=confidence; f->status=IDENTITY_REVIEW_PROPOSED;
|
f->confidence=confidence; f->status=IDENTITY_REVIEW_PROPOSED;
|
||||||
f->origin=g_strdup("ocr"); f->value_quality=g_strdup("complete"); f->order=order;
|
f->origin=g_strdup("ocr"); f->value_quality=g_strdup("complete"); f->order=order;
|
||||||
if (box != NULL) f->box=*box;
|
if (box != NULL) f->box=*box;
|
||||||
|
|
@ -60,7 +60,7 @@ IdentityFieldObservation *identity_field_observation_new_manual(
|
||||||
value[0] == '\0') return NULL;
|
value[0] == '\0') return NULL;
|
||||||
IdentityFieldObservation *field =
|
IdentityFieldObservation *field =
|
||||||
g_new0(IdentityFieldObservation, 1);
|
g_new0(IdentityFieldObservation, 1);
|
||||||
field->code = g_strdup(code);
|
field->id = g_uuid_string_random();field->code = g_strdup(code);
|
||||||
field->corrected_value = g_strdup(value);
|
field->corrected_value = g_strdup(value);
|
||||||
field->confidence = -1.0;
|
field->confidence = -1.0;
|
||||||
field->status = IDENTITY_REVIEW_PROPOSED;
|
field->status = IDENTITY_REVIEW_PROPOSED;
|
||||||
|
|
@ -79,6 +79,7 @@ IdentityFieldObservation *identity_field_observation_copy(
|
||||||
: identity_field_observation_new_manual(
|
: identity_field_observation_new_manual(
|
||||||
f->code,f->corrected_value,f->order);
|
f->code,f->corrected_value,f->order);
|
||||||
c->corrected_value=g_strdup(f->corrected_value);
|
c->corrected_value=g_strdup(f->corrected_value);
|
||||||
|
g_free(c->id);c->id=g_strdup(f->id);
|
||||||
c->normalized_value=g_strdup(f->normalized_value);
|
c->normalized_value=g_strdup(f->normalized_value);
|
||||||
c->confirmed_value=g_strdup(f->confirmed_value);
|
c->confirmed_value=g_strdup(f->confirmed_value);
|
||||||
g_free(c->value_quality);c->value_quality=g_strdup(f->value_quality);
|
g_free(c->value_quality);c->value_quality=g_strdup(f->value_quality);
|
||||||
|
|
@ -88,7 +89,7 @@ IdentityFieldObservation *identity_field_observation_copy(
|
||||||
void identity_field_observation_free(IdentityFieldObservation *f)
|
void identity_field_observation_free(IdentityFieldObservation *f)
|
||||||
{
|
{
|
||||||
if(f==NULL)return;
|
if(f==NULL)return;
|
||||||
g_free(f->code);g_free(f->raw_value);
|
g_free(f->id);g_free(f->code);g_free(f->raw_value);
|
||||||
g_free(f->corrected_value);g_free(f->normalized_value);g_free(f->note);
|
g_free(f->corrected_value);g_free(f->normalized_value);g_free(f->note);
|
||||||
g_free(f->origin);g_free(f->confirmed_value);g_free(f->value_quality);g_free(f);
|
g_free(f->origin);g_free(f->confirmed_value);g_free(f->value_quality);g_free(f);
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +130,7 @@ void identity_field_observation_mark_conflict(IdentityFieldObservation*f)
|
||||||
g_clear_pointer(&f->confirmed_value,g_free);}}
|
g_clear_pointer(&f->confirmed_value,g_free);}}
|
||||||
#define FG(name,type,field,zero) type identity_field_observation_get_##name(\
|
#define FG(name,type,field,zero) type identity_field_observation_get_##name(\
|
||||||
const IdentityFieldObservation*f){return f!=NULL?f->field:zero;}
|
const IdentityFieldObservation*f){return f!=NULL?f->field:zero;}
|
||||||
FG(code,const char*,code,NULL) FG(raw_value,const char*,raw_value,NULL)
|
FG(identifier,const char*,id,NULL) FG(code,const char*,code,NULL) FG(raw_value,const char*,raw_value,NULL)
|
||||||
FG(corrected_value,const char*,corrected_value,NULL)
|
FG(corrected_value,const char*,corrected_value,NULL)
|
||||||
FG(normalized_value,const char*,normalized_value,NULL)
|
FG(normalized_value,const char*,normalized_value,NULL)
|
||||||
FG(confirmed_value,const char*,confirmed_value,NULL)
|
FG(confirmed_value,const char*,confirmed_value,NULL)
|
||||||
|
|
@ -140,6 +141,8 @@ FG(confidence,double,confidence,-1.0)
|
||||||
FG(order,guint,order,0)
|
FG(order,guint,order,0)
|
||||||
const IdentitySourceBox *identity_field_observation_get_box(
|
const IdentitySourceBox *identity_field_observation_get_box(
|
||||||
const IdentityFieldObservation*f){return f!=NULL?&f->box:NULL;}
|
const IdentityFieldObservation*f){return f!=NULL?&f->box:NULL;}
|
||||||
|
gboolean identity_field_observation_replace_identifier(IdentityFieldObservation*f,const char*id)
|
||||||
|
{if(!f||!g_uuid_string_is_valid(id))return FALSE;g_free(f->id);f->id=g_strdup(id);return TRUE;}
|
||||||
gboolean identity_field_observation_set_normalized_value(
|
gboolean identity_field_observation_set_normalized_value(
|
||||||
IdentityFieldObservation*f,const char*v)
|
IdentityFieldObservation*f,const char*v)
|
||||||
{if(!f||!v||!v[0]||!g_utf8_validate(v,-1,NULL))return FALSE;
|
{if(!f||!v||!v[0]||!g_utf8_validate(v,-1,NULL))return FALSE;
|
||||||
|
|
|
||||||
97
src/models/person_ocr_projection.c
Normal file
97
src/models/person_ocr_projection.c
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
#include "models/person_ocr_projection.h"
|
||||||
|
struct PersonOcrFieldProjection {
|
||||||
|
char *evidence, *run, *field, *ocr_code, *value, *quality, *status, *target,
|
||||||
|
*current;
|
||||||
|
PersonOcrProjectionStrategy strategy;
|
||||||
|
gboolean confirmed;
|
||||||
|
};
|
||||||
|
static gboolean text(const char *v) {
|
||||||
|
return v && *v && g_utf8_validate(v, -1, NULL);
|
||||||
|
}
|
||||||
|
PersonOcrFieldProjection *person_ocr_field_projection_new(
|
||||||
|
const char *e, const char *r, const char *f, const char *c, const char *v,
|
||||||
|
const char *q, const char *s, const char *t, const char *current,
|
||||||
|
PersonOcrProjectionStrategy strategy, gboolean confirmed) {
|
||||||
|
PersonOcrFieldProjection *p = g_new0(PersonOcrFieldProjection, 1);
|
||||||
|
p->evidence = g_strdup(e);
|
||||||
|
p->run = g_strdup(r);
|
||||||
|
p->field = g_strdup(f);
|
||||||
|
p->ocr_code = g_strdup(c);
|
||||||
|
p->value = g_strdup(v);
|
||||||
|
p->quality = g_strdup(q);
|
||||||
|
p->status = g_strdup(s);
|
||||||
|
p->target = g_strdup(t);
|
||||||
|
p->current = g_strdup(current);
|
||||||
|
p->strategy = strategy;
|
||||||
|
p->confirmed = confirmed;
|
||||||
|
if (!person_ocr_field_projection_is_valid(p)) {
|
||||||
|
person_ocr_field_projection_free(p);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
gboolean
|
||||||
|
person_ocr_field_projection_is_valid(const PersonOcrFieldProjection *p) {
|
||||||
|
return p && g_uuid_string_is_valid(p->evidence) &&
|
||||||
|
g_uuid_string_is_valid(p->run) && g_uuid_string_is_valid(p->field) &&
|
||||||
|
text(p->ocr_code) && text(p->value) && text(p->quality) &&
|
||||||
|
text(p->status) && text(p->target) &&
|
||||||
|
(p->current == NULL || g_utf8_validate(p->current, -1, NULL)) &&
|
||||||
|
p->confirmed &&
|
||||||
|
(g_str_equal(p->quality, "complete") ||
|
||||||
|
g_str_equal(p->quality, "partial")) &&
|
||||||
|
(g_str_equal(p->status, "accepted") ||
|
||||||
|
g_str_equal(p->status, "modified")) &&
|
||||||
|
(p->strategy == PERSON_OCR_KEEP_EXISTING ||
|
||||||
|
p->strategy == PERSON_OCR_FILL_EMPTY ||
|
||||||
|
p->strategy == PERSON_OCR_REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
PersonOcrFieldProjection *
|
||||||
|
person_ocr_field_projection_copy(const PersonOcrFieldProjection *p) {
|
||||||
|
return p ? person_ocr_field_projection_new(p->evidence, p->run, p->field,
|
||||||
|
p->ocr_code, p->value, p->quality,
|
||||||
|
p->status, p->target, p->current,
|
||||||
|
p->strategy, p->confirmed)
|
||||||
|
: NULL;
|
||||||
|
}
|
||||||
|
void person_ocr_field_projection_free(PersonOcrFieldProjection *p) {
|
||||||
|
if (!p)
|
||||||
|
return;
|
||||||
|
g_free(p->evidence);
|
||||||
|
g_free(p->run);
|
||||||
|
g_free(p->field);
|
||||||
|
g_free(p->ocr_code);
|
||||||
|
g_free(p->value);
|
||||||
|
g_free(p->quality);
|
||||||
|
g_free(p->status);
|
||||||
|
g_free(p->target);
|
||||||
|
g_free(p->current);
|
||||||
|
g_free(p);
|
||||||
|
}
|
||||||
|
#define GET(name, field) \
|
||||||
|
const char *person_ocr_field_projection_get_##name( \
|
||||||
|
const PersonOcrFieldProjection *p) { \
|
||||||
|
return p ? p->field : NULL; \
|
||||||
|
}
|
||||||
|
GET(evidence_id, evidence)
|
||||||
|
GET(run_id, run) GET(ocr_field_id, field) GET(ocr_code, ocr_code)
|
||||||
|
GET(confirmed_value, value) GET(quality, quality) GET(review_status, status)
|
||||||
|
GET(person_field, target)
|
||||||
|
GET(current_value, current) PersonOcrProjectionStrategy
|
||||||
|
person_ocr_field_projection_get_strategy(
|
||||||
|
const PersonOcrFieldProjection *p) {
|
||||||
|
return p ? p->strategy : PERSON_OCR_KEEP_EXISTING;
|
||||||
|
}
|
||||||
|
gboolean person_ocr_field_projection_get_human_confirmed(
|
||||||
|
const PersonOcrFieldProjection *p) {
|
||||||
|
return p && p->confirmed;
|
||||||
|
}
|
||||||
|
gboolean
|
||||||
|
person_ocr_field_projection_replace_evidence_id(PersonOcrFieldProjection *p,
|
||||||
|
const char *e) {
|
||||||
|
if (!p || !g_uuid_string_is_valid(e))
|
||||||
|
return FALSE;
|
||||||
|
g_free(p->evidence);
|
||||||
|
p->evidence = g_strdup(e);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
/******************************************************************************
|
|
||||||
* @file create_person_dialog.c
|
|
||||||
* @brief Formulaire GTK de création d'une personne observée.
|
|
||||||
******************************************************************************/
|
|
||||||
#include "views/create_person_dialog.h"
|
#include "views/create_person_dialog.h"
|
||||||
#include "views/dialog_geometry.h"
|
#include "views/dialog_geometry.h"
|
||||||
#include "views/person_vocabulary_adapter.h"
|
#include "views/person_vocabulary_adapter.h"
|
||||||
#include "views/identity_ocr_option_adapter.h"
|
#include "views/identity_ocr_option_adapter.h"
|
||||||
#include "views/person_factual_relation_editor.h"
|
#include "views/person_factual_relation_editor.h"
|
||||||
|
#include "views/person_ocr_projection_editor.h"
|
||||||
|
#include "views/person_creation_confirmation.h"
|
||||||
#include "core/evidence_staging.h"
|
#include "core/evidence_staging.h"
|
||||||
#include "core/evidence_staging_task.h"
|
#include "core/evidence_staging_task.h"
|
||||||
#include "core/person_confirmation_summary.h"
|
#include "core/person_confirmation_summary.h"
|
||||||
|
|
@ -35,6 +33,7 @@ struct CreatePersonDialogResult
|
||||||
EvidenceStaging *staging;
|
EvidenceStaging *staging;
|
||||||
GPtrArray *ocr_runs;
|
GPtrArray *ocr_runs;
|
||||||
GPtrArray *factual_relations;
|
GPtrArray *factual_relations;
|
||||||
|
GPtrArray *ocr_projections;
|
||||||
};
|
};
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
|
|
@ -110,6 +109,7 @@ typedef struct
|
||||||
gboolean updating_evidence;
|
gboolean updating_evidence;
|
||||||
gboolean updating_retained;
|
gboolean updating_retained;
|
||||||
PersonFactualRelationEditor *factual_relation_editor;
|
PersonFactualRelationEditor *factual_relation_editor;
|
||||||
|
PersonOcrProjectionEditor *ocr_projection_editor;
|
||||||
} CreatePersonDialogState;
|
} CreatePersonDialogState;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
@ -204,7 +204,6 @@ static void identity_languages_completed(GObject *source,
|
||||||
g_clear_object(&window);
|
g_clear_object(&window);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Extrait les notes de la zone de texte. */
|
|
||||||
static char *create_person_dialog_get_notes(CreatePersonDialogState *state)
|
static char *create_person_dialog_get_notes(CreatePersonDialogState *state)
|
||||||
{
|
{
|
||||||
GtkTextBuffer *buffer = gtk_text_view_get_buffer(state->notes);
|
GtkTextBuffer *buffer = gtk_text_view_get_buffer(state->notes);
|
||||||
|
|
@ -676,7 +675,6 @@ static void create_person_dialog_on_ocr_start(GtkButton *button,
|
||||||
"OCR contrôlé en cours… Aucune donnée n’est encore persistée.");
|
"OCR contrôlé en cours… Aucune donnée n’est encore persistée.");
|
||||||
g_task_run_in_thread(state->ocr_task, create_person_dialog_ocr_worker);
|
g_task_run_in_thread(state->ocr_task, create_person_dialog_ocr_worker);
|
||||||
}
|
}
|
||||||
/** @brief Libère l'état attaché à la fenêtre. */
|
|
||||||
static void create_person_dialog_state_free(gpointer data)
|
static void create_person_dialog_state_free(gpointer data)
|
||||||
{
|
{
|
||||||
CreatePersonDialogState *state = data;
|
CreatePersonDialogState *state = data;
|
||||||
|
|
@ -712,6 +710,7 @@ static void create_person_dialog_state_free(gpointer data)
|
||||||
g_free(state->tesseract_path);
|
g_free(state->tesseract_path);
|
||||||
g_free(state->tesseract_version);
|
g_free(state->tesseract_version);
|
||||||
person_factual_relation_editor_free(state->factual_relation_editor);
|
person_factual_relation_editor_free(state->factual_relation_editor);
|
||||||
|
person_ocr_projection_editor_free(state->ocr_projection_editor);
|
||||||
if (state->user_data_destroy != NULL)
|
if (state->user_data_destroy != NULL)
|
||||||
state->user_data_destroy(state->user_data);
|
state->user_data_destroy(state->user_data);
|
||||||
g_free(state);
|
g_free(state);
|
||||||
|
|
@ -1170,7 +1169,6 @@ static void create_person_dialog_on_evidence_changed(
|
||||||
state->selection_model, identifier);
|
state->selection_model, identifier);
|
||||||
create_person_dialog_select_record(state, record, NULL);
|
create_person_dialog_select_record(state, record, NULL);
|
||||||
}
|
}
|
||||||
/** @brief Termine le dialogue comme une annulation. */
|
|
||||||
static void create_person_dialog_cancel(CreatePersonDialogState *state)
|
static void create_person_dialog_cancel(CreatePersonDialogState *state)
|
||||||
{
|
{
|
||||||
if (state == NULL ||
|
if (state == NULL ||
|
||||||
|
|
@ -1231,18 +1229,15 @@ static void create_person_dialog_select_record(CreatePersonDialogState *state,
|
||||||
evidence_preview_request_free(request);
|
evidence_preview_request_free(request);
|
||||||
g_free(size); g_free(sha); g_free(text);
|
g_free(size); g_free(sha); g_free(text);
|
||||||
}
|
}
|
||||||
/** @brief Traite la fermeture native. */
|
|
||||||
static gboolean create_person_dialog_on_close(GtkWindow *window, gpointer data)
|
static gboolean create_person_dialog_on_close(GtkWindow *window, gpointer data)
|
||||||
{
|
{
|
||||||
(void) window; create_person_dialog_cancel(data); return FALSE;
|
(void) window; create_person_dialog_cancel(data); return FALSE;
|
||||||
}
|
}
|
||||||
/** @brief Traite le bouton Annuler. */
|
|
||||||
static void create_person_dialog_on_cancel(GtkButton *button, gpointer data)
|
static void create_person_dialog_on_cancel(GtkButton *button, gpointer data)
|
||||||
{
|
{
|
||||||
CreatePersonDialogState *state = data;
|
CreatePersonDialogState *state = data;
|
||||||
(void) button; create_person_dialog_cancel(state); gtk_window_close(state->window);
|
(void) button; create_person_dialog_cancel(state); gtk_window_close(state->window);
|
||||||
}
|
}
|
||||||
/** @brief Valide puis transmet les valeurs du formulaire. */
|
|
||||||
static void create_person_dialog_on_create(GtkButton *button, gpointer data)
|
static void create_person_dialog_on_create(GtkButton *button, gpointer data)
|
||||||
{
|
{
|
||||||
CreatePersonDialogState *state = data;
|
CreatePersonDialogState *state = data;
|
||||||
|
|
@ -1304,6 +1299,11 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
|
||||||
g_clear_error(&relation_error); create_person_dialog_result_free(result);
|
g_clear_error(&relation_error); create_person_dialog_result_free(result);
|
||||||
g_free(notes); return;
|
g_free(notes); return;
|
||||||
}
|
}
|
||||||
|
if (!person_ocr_projection_editor_collect(state->ocr_projection_editor,&result->ocr_projections,&relation_error)) {
|
||||||
|
gtk_label_set_text(state->error, relation_error->message);
|
||||||
|
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
||||||
|
g_clear_error(&relation_error);create_person_dialog_result_free(result);g_free(notes);return;
|
||||||
|
}
|
||||||
result->evidence_selection = state->person_evidence_selection;
|
result->evidence_selection = state->person_evidence_selection;
|
||||||
state->person_evidence_selection = NULL;
|
state->person_evidence_selection = NULL;
|
||||||
result->staging = state->staging;
|
result->staging = state->staging;
|
||||||
|
|
@ -1323,10 +1323,11 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
|
||||||
static void create_person_dialog_update_navigation(CreatePersonDialogState *state)
|
static void create_person_dialog_update_navigation(CreatePersonDialogState *state)
|
||||||
{
|
{
|
||||||
static const char *const pages[] = {
|
static const char *const pages[] = {
|
||||||
"person", "roles", "evidence", "identity-ocr", "factual-relations", "summary"};
|
"person", "roles", "evidence", "identity-ocr", "ocr-projection",
|
||||||
|
"factual-relations", "summary"};
|
||||||
static const char *const steps[] = {
|
static const char *const steps[] = {
|
||||||
"1 Personne", "2 Rôles", "3 Preuves", "4 OCR identité",
|
"1 Personne", "2 Rôles", "3 Preuves", "4 OCR identité",
|
||||||
"5 Relations factuelles", "6 Confirmation"};
|
"5 Projection OCR", "6 Relations factuelles", "7 Confirmation"};
|
||||||
GString *progress = g_string_new(NULL);
|
GString *progress = g_string_new(NULL);
|
||||||
gtk_stack_set_visible_child_name(state->stack, pages[state->step]);
|
gtk_stack_set_visible_child_name(state->stack, pages[state->step]);
|
||||||
for (guint i = 0; i < G_N_ELEMENTS(steps); i++) {
|
for (guint i = 0; i < G_N_ELEMENTS(steps); i++) {
|
||||||
|
|
@ -1340,7 +1341,7 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat
|
||||||
}
|
}
|
||||||
gtk_label_set_markup(state->progress, progress->str);
|
gtk_label_set_markup(state->progress, progress->str);
|
||||||
g_string_free(progress, TRUE);
|
g_string_free(progress, TRUE);
|
||||||
if (state->step == 5) {
|
if (state->step == 6) {
|
||||||
GPtrArray *role_labels =
|
GPtrArray *role_labels =
|
||||||
person_vocabulary_adapter_selected_role_labels(
|
person_vocabulary_adapter_selected_role_labels(
|
||||||
state->role_buttons);
|
state->role_buttons);
|
||||||
|
|
@ -1358,52 +1359,9 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat
|
||||||
gtk_spin_button_get_value_as_int(state->confidence), notes,
|
gtk_spin_button_get_value_as_int(state->confidence), notes,
|
||||||
role_labels, state->person_evidence_selection);
|
role_labels, state->person_evidence_selection);
|
||||||
GString *confirmation = g_string_new(text);
|
GString *confirmation = g_string_new(text);
|
||||||
person_factual_relation_editor_append_summary(state->factual_relation_editor,
|
person_creation_confirmation_append_sections(confirmation,
|
||||||
confirmation);
|
state->ocr_runs, state->ocr_projection_editor,
|
||||||
g_string_append(confirmation,
|
state->factual_relation_editor);
|
||||||
"\n\nOCR identité\nL’OCR peut contenir des erreurs. Ces "
|
|
||||||
"informations sont seulement présentées sur le document ; "
|
|
||||||
"l’authenticité n’est pas établie et aucune fusion de personne "
|
|
||||||
"ne sera effectuée. Aucune écriture n’a encore eu lieu.");
|
|
||||||
for (guint run_index = 0;
|
|
||||||
state->ocr_runs != NULL && run_index < state->ocr_runs->len;
|
|
||||||
run_index++) {
|
|
||||||
IdentityOcrRun *run = g_ptr_array_index(
|
|
||||||
state->ocr_runs, run_index);
|
|
||||||
g_string_append_printf(confirmation,
|
|
||||||
"\n• %s — %s — page %u — Tesseract %s — %s",
|
|
||||||
identity_ocr_run_get_document_type(run),
|
|
||||||
identity_ocr_run_get_document_side(run),
|
|
||||||
identity_ocr_run_get_page(run),
|
|
||||||
identity_ocr_run_get_version(run) != NULL
|
|
||||||
? identity_ocr_run_get_version(run) : "version inconnue",
|
|
||||||
identity_ocr_run_get_languages(run));
|
|
||||||
if (identity_ocr_run_get_factual_notes(run) != NULL)
|
|
||||||
g_string_append_printf(confirmation,
|
|
||||||
"\n Notes factuelles : %s",
|
|
||||||
identity_ocr_run_get_factual_notes(run));
|
|
||||||
const GPtrArray *fields = identity_ocr_run_get_fields(run);
|
|
||||||
for (guint field_index = 0;
|
|
||||||
fields != NULL && field_index < fields->len; field_index++) {
|
|
||||||
IdentityFieldObservation *field =
|
|
||||||
g_ptr_array_index((GPtrArray *) fields, field_index);
|
|
||||||
IdentityReviewStatus review =
|
|
||||||
identity_field_observation_get_status(field);
|
|
||||||
if (review == IDENTITY_REVIEW_ACCEPTED ||
|
|
||||||
review == IDENTITY_REVIEW_MODIFIED)
|
|
||||||
g_string_append_printf(confirmation,
|
|
||||||
"\n - %s : %s (brut : %s ; origine : %s)",
|
|
||||||
identity_field_observation_get_code(field),
|
|
||||||
identity_field_observation_get_corrected_value(field)
|
|
||||||
!= NULL
|
|
||||||
? identity_field_observation_get_corrected_value(field)
|
|
||||||
: identity_field_observation_get_raw_value(field),
|
|
||||||
identity_field_observation_get_raw_value(field) != NULL
|
|
||||||
? identity_field_observation_get_raw_value(field)
|
|
||||||
: "absente",
|
|
||||||
identity_field_observation_get_origin(field));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gtk_label_set_text(state->summary, confirmation->str);
|
gtk_label_set_text(state->summary, confirmation->str);
|
||||||
GtkAdjustment *adjustment = gtk_scrolled_window_get_vadjustment(
|
GtkAdjustment *adjustment = gtk_scrolled_window_get_vadjustment(
|
||||||
state->summary_scroll);
|
state->summary_scroll);
|
||||||
|
|
@ -1415,10 +1373,10 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat
|
||||||
g_ptr_array_unref(role_labels);
|
g_ptr_array_unref(role_labels);
|
||||||
}
|
}
|
||||||
gtk_widget_set_sensitive(GTK_WIDGET(state->previous), state->step > 0);
|
gtk_widget_set_sensitive(GTK_WIDGET(state->previous), state->step > 0);
|
||||||
gtk_widget_set_visible(GTK_WIDGET(state->next), state->step < 5);
|
gtk_widget_set_visible(GTK_WIDGET(state->next), state->step < 6);
|
||||||
gtk_widget_set_visible(GTK_WIDGET(state->create), state->step == 5);
|
gtk_widget_set_visible(GTK_WIDGET(state->create), state->step == 6);
|
||||||
gtk_widget_set_sensitive(GTK_WIDGET(state->create),
|
gtk_widget_set_sensitive(GTK_WIDGET(state->create),
|
||||||
state->step == 5 &&
|
state->step == 6 &&
|
||||||
gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] != '\0' &&
|
gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] != '\0' &&
|
||||||
person_evidence_selection_is_confirmable(
|
person_evidence_selection_is_confirmable(
|
||||||
state->person_evidence_selection));
|
state->person_evidence_selection));
|
||||||
|
|
@ -1439,11 +1397,24 @@ static void create_person_dialog_on_next(GtkButton *button, gpointer data)
|
||||||
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state->step == 3)
|
if (state->step == 3) {
|
||||||
create_person_dialog_capture_corrected_transcription(state);
|
create_person_dialog_capture_corrected_transcription(state);
|
||||||
if (state->step == 3)
|
|
||||||
create_person_dialog_capture_factual_notes(state);
|
create_person_dialog_capture_factual_notes(state);
|
||||||
|
person_ocr_projection_editor_set_runs(state->ocr_projection_editor,
|
||||||
|
state->ocr_runs);
|
||||||
|
}
|
||||||
if (state->step == 4) {
|
if (state->step == 4) {
|
||||||
|
GError *error = NULL;
|
||||||
|
GPtrArray *projections = NULL;
|
||||||
|
if (!person_ocr_projection_editor_collect(state->ocr_projection_editor,
|
||||||
|
&projections, &error)) {
|
||||||
|
gtk_label_set_text(state->error, error->message);
|
||||||
|
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
||||||
|
g_clear_error(&error); return;
|
||||||
|
}
|
||||||
|
g_ptr_array_unref(projections);
|
||||||
|
}
|
||||||
|
if (state->step == 5) {
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
if (!person_factual_relation_editor_validate(state->factual_relation_editor,
|
if (!person_factual_relation_editor_validate(state->factual_relation_editor,
|
||||||
&error)) {
|
&error)) {
|
||||||
|
|
@ -1453,10 +1424,9 @@ static void create_person_dialog_on_next(GtkButton *button, gpointer data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
|
gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
|
||||||
if (state->step < 5) state->step++;
|
if (state->step < 6) state->step++;
|
||||||
create_person_dialog_update_navigation(state);
|
create_person_dialog_update_navigation(state);
|
||||||
}
|
}
|
||||||
/** @brief Ajoute une ligne libellée au formulaire. */
|
|
||||||
static void create_person_dialog_add_row(GtkGrid *grid, int row,
|
static void create_person_dialog_add_row(GtkGrid *grid, int row,
|
||||||
const char *label, GtkWidget *widget)
|
const char *label, GtkWidget *widget)
|
||||||
{
|
{
|
||||||
|
|
@ -1583,6 +1553,8 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
||||||
create_person_dialog_add_row(GTK_GRID(grid), 4, "Confiance (%)", GTK_WIDGET(state->confidence));
|
create_person_dialog_add_row(GTK_GRID(grid), 4, "Confiance (%)", GTK_WIDGET(state->confidence));
|
||||||
create_person_dialog_add_row(GTK_GRID(grid), 5, "Notes factuelles", GTK_WIDGET(state->notes));
|
create_person_dialog_add_row(GTK_GRID(grid), 5, "Notes factuelles", GTK_WIDGET(state->notes));
|
||||||
state->stack = GTK_STACK(gtk_stack_new());
|
state->stack = GTK_STACK(gtk_stack_new());
|
||||||
|
gtk_widget_set_name(GTK_WIDGET(state->stack),
|
||||||
|
"create-person-assistant-stack");
|
||||||
gtk_stack_set_transition_type(state->stack,
|
gtk_stack_set_transition_type(state->stack,
|
||||||
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
|
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
|
||||||
gtk_stack_add_titled(state->stack, grid, "person", "1 — Personne");
|
gtk_stack_add_titled(state->stack, grid, "person", "1 — Personne");
|
||||||
|
|
@ -1861,19 +1833,28 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
||||||
gtk_scrolled_window_set_child(state->summary_scroll, summary);
|
gtk_scrolled_window_set_child(state->summary_scroll, summary);
|
||||||
|
|
||||||
state->factual_relation_editor = person_factual_relation_editor_new();
|
state->factual_relation_editor = person_factual_relation_editor_new();
|
||||||
|
state->ocr_projection_editor = person_ocr_projection_editor_new();
|
||||||
person_factual_relation_editor_set_available_evidence(state->factual_relation_editor,
|
person_factual_relation_editor_set_available_evidence(state->factual_relation_editor,
|
||||||
state->evidence_labels, state->evidence_identifiers);
|
state->evidence_labels, state->evidence_identifiers);
|
||||||
person_factual_relation_editor_set_available_ocr_runs(state->factual_relation_editor,
|
person_factual_relation_editor_set_available_ocr_runs(state->factual_relation_editor,
|
||||||
state->ocr_runs);
|
state->ocr_runs);
|
||||||
GtkWidget *relation_scroll = gtk_scrolled_window_new(); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(relation_scroll),
|
GtkWidget *projection_scroll = gtk_scrolled_window_new();
|
||||||
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(projection_scroll),
|
||||||
|
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||||
|
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(projection_scroll),
|
||||||
|
person_ocr_projection_editor_get_widget(state->ocr_projection_editor));
|
||||||
|
gtk_stack_add_titled(state->stack, projection_scroll, "ocr-projection",
|
||||||
|
"5 — Projection OCR");
|
||||||
|
GtkWidget *relation_scroll = gtk_scrolled_window_new();
|
||||||
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(relation_scroll),
|
||||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(relation_scroll),
|
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(relation_scroll),
|
||||||
person_factual_relation_editor_get_widget(state->factual_relation_editor));
|
person_factual_relation_editor_get_widget(state->factual_relation_editor));
|
||||||
gtk_stack_add_titled(state->stack, relation_scroll, "factual-relations",
|
gtk_stack_add_titled(state->stack, relation_scroll, "factual-relations",
|
||||||
"5 — Relations factuelles");
|
"6 — Relations factuelles");
|
||||||
|
|
||||||
gtk_stack_add_titled(state->stack,
|
gtk_stack_add_titled(state->stack,
|
||||||
GTK_WIDGET(state->summary_scroll), "summary", "6 — Confirmation");
|
GTK_WIDGET(state->summary_scroll), "summary", "7 — Confirmation");
|
||||||
state->error = GTK_LABEL(gtk_label_new(NULL)); gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
|
state->error = GTK_LABEL(gtk_label_new(NULL)); gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
|
||||||
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||||
gtk_widget_set_name(actions, "create-person-actions");
|
gtk_widget_set_name(actions, "create-person-actions");
|
||||||
|
|
@ -1955,6 +1936,7 @@ void create_person_dialog_result_free(CreatePersonDialogResult *result)
|
||||||
if (result->factual_relations) {
|
if (result->factual_relations) {
|
||||||
g_ptr_array_unref(result->factual_relations);
|
g_ptr_array_unref(result->factual_relations);
|
||||||
}
|
}
|
||||||
|
g_clear_pointer(&result->ocr_projections, g_ptr_array_unref);
|
||||||
g_free(result);
|
g_free(result);
|
||||||
}
|
}
|
||||||
const PersonEntityInput *create_person_dialog_result_get_input(
|
const PersonEntityInput *create_person_dialog_result_get_input(
|
||||||
|
|
@ -1985,16 +1967,6 @@ const GPtrArray *create_person_dialog_result_get_factual_relations(
|
||||||
{
|
{
|
||||||
return result != NULL ? result->factual_relations : NULL;
|
return result != NULL ? result->factual_relations : NULL;
|
||||||
}
|
}
|
||||||
gboolean create_person_dialog_test_overlay_has_region(GtkWindow *window)
|
const GPtrArray *create_person_dialog_result_get_ocr_projections(const CreatePersonDialogResult *r){return r?r->ocr_projections:NULL;}
|
||||||
{
|
gboolean create_person_dialog_test_overlay_has_region(GtkWindow*w){CreatePersonDialogState*s=w?g_object_get_data(G_OBJECT(w),"person-dialog-state"):NULL;return s&&ocr_provenance_overlay_has_region(s->ocr_overlay);}
|
||||||
CreatePersonDialogState *state = window != NULL
|
guint64 create_person_dialog_test_ocr_generation(GtkWindow*w){CreatePersonDialogState*s=w?g_object_get_data(G_OBJECT(w),"person-dialog-state"):NULL;return s?s->ocr_generation:0;}
|
||||||
? g_object_get_data(G_OBJECT(window), "person-dialog-state") : NULL;
|
|
||||||
return state != NULL &&
|
|
||||||
ocr_provenance_overlay_has_region(state->ocr_overlay);
|
|
||||||
}
|
|
||||||
guint64 create_person_dialog_test_ocr_generation(GtkWindow *window)
|
|
||||||
{
|
|
||||||
CreatePersonDialogState *state = window != NULL
|
|
||||||
? g_object_get_data(G_OBJECT(window), "person-dialog-state") : NULL;
|
|
||||||
return state != NULL ? state->ocr_generation : 0;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1965,8 +1965,8 @@ void main_window_set_entity_selected_callback(MainWindow *main_window,
|
||||||
void main_window_set_person_evidences(MainWindow *main_window, const GPtrArray *records) {
|
void main_window_set_person_evidences(MainWindow *main_window, const GPtrArray *records) {
|
||||||
if (main_window) workspace_set_person_evidences(main_window->workspace, records);
|
if (main_window) workspace_set_person_evidences(main_window->workspace, records);
|
||||||
}
|
}
|
||||||
void main_window_set_person_factual_relations(MainWindow *main_window, const GPtrArray *relations, const GPtrArray *evidence_records) {
|
void main_window_set_person_factual_relations(MainWindow *main_window, const GPtrArray *relations, const GPtrArray *evidence_records,const GHashTable *profile_fields) {
|
||||||
if (main_window) workspace_set_person_factual_relations(main_window->workspace, relations, evidence_records);
|
if (main_window) workspace_set_person_factual_relations(main_window->workspace, relations, evidence_records,profile_fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
void main_window_set_quit_callback(
|
void main_window_set_quit_callback(
|
||||||
|
|
|
||||||
65
src/views/person_creation_confirmation.c
Normal file
65
src/views/person_creation_confirmation.c
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#include "views/person_creation_confirmation.h"
|
||||||
|
|
||||||
|
static const char *displayed_value(const IdentityFieldObservation *field)
|
||||||
|
{
|
||||||
|
const char *corrected = identity_field_observation_get_corrected_value(field);
|
||||||
|
return corrected != NULL ? corrected
|
||||||
|
: identity_field_observation_get_raw_value(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void append_ocr_review(GString *summary, const GPtrArray *runs)
|
||||||
|
{
|
||||||
|
g_string_append(summary,
|
||||||
|
"\n\nCorrection OCR enregistrée\n"
|
||||||
|
"La transcription corrigée reste attachée à la preuve et distincte "
|
||||||
|
"des données appliquées à la personne. L’OCR peut contenir des "
|
||||||
|
"erreurs : l’authenticité n’est pas établie.");
|
||||||
|
if (runs == NULL || runs->len == 0) {
|
||||||
|
g_string_append(summary, "\nAucune correction OCR enregistrée.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (guint run_index = 0; run_index < runs->len; run_index++) {
|
||||||
|
IdentityOcrRun *run = g_ptr_array_index((GPtrArray *) runs, run_index);
|
||||||
|
g_string_append_printf(summary,
|
||||||
|
"\n• %s — %s — page %u — Tesseract %s — %s",
|
||||||
|
identity_ocr_run_get_document_type(run),
|
||||||
|
identity_ocr_run_get_document_side(run),
|
||||||
|
identity_ocr_run_get_page(run),
|
||||||
|
identity_ocr_run_get_version(run) != NULL
|
||||||
|
? identity_ocr_run_get_version(run) : "version inconnue",
|
||||||
|
identity_ocr_run_get_languages(run));
|
||||||
|
if (identity_ocr_run_get_factual_notes(run) != NULL)
|
||||||
|
g_string_append_printf(summary, "\n Notes factuelles : %s",
|
||||||
|
identity_ocr_run_get_factual_notes(run));
|
||||||
|
const GPtrArray *fields = identity_ocr_run_get_fields(run);
|
||||||
|
for (guint field_index = 0; fields != NULL && field_index < fields->len;
|
||||||
|
field_index++) {
|
||||||
|
IdentityFieldObservation *field =
|
||||||
|
g_ptr_array_index((GPtrArray *) fields, field_index);
|
||||||
|
IdentityReviewStatus review =
|
||||||
|
identity_field_observation_get_status(field);
|
||||||
|
if (review == IDENTITY_REVIEW_ACCEPTED ||
|
||||||
|
review == IDENTITY_REVIEW_MODIFIED)
|
||||||
|
g_string_append_printf(summary,
|
||||||
|
"\n - %s : %s (brut : %s ; origine : %s)",
|
||||||
|
identity_field_observation_get_code(field),
|
||||||
|
displayed_value(field),
|
||||||
|
identity_field_observation_get_raw_value(field) != NULL
|
||||||
|
? identity_field_observation_get_raw_value(field)
|
||||||
|
: "absente",
|
||||||
|
identity_field_observation_get_origin(field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void person_creation_confirmation_append_sections(
|
||||||
|
GString *summary, const GPtrArray *ocr_runs,
|
||||||
|
PersonOcrProjectionEditor *projection_editor,
|
||||||
|
PersonFactualRelationEditor *relation_editor)
|
||||||
|
{
|
||||||
|
if (summary == NULL)
|
||||||
|
return;
|
||||||
|
append_ocr_review(summary, ocr_runs);
|
||||||
|
person_ocr_projection_editor_append_summary(projection_editor, summary);
|
||||||
|
person_factual_relation_editor_append_summary(relation_editor, summary);
|
||||||
|
}
|
||||||
|
|
@ -157,6 +157,7 @@ PersonFactualRelationEditor *person_factual_relation_editor_new(void)
|
||||||
PersonFactualRelationEditor *editor=g_new0(PersonFactualRelationEditor,1);
|
PersonFactualRelationEditor *editor=g_new0(PersonFactualRelationEditor,1);
|
||||||
editor->rows=g_ptr_array_new_with_free_func(row_free);
|
editor->rows=g_ptr_array_new_with_free_func(row_free);
|
||||||
editor->root=gtk_box_new(GTK_ORIENTATION_VERTICAL,8);
|
editor->root=gtk_box_new(GTK_ORIENTATION_VERTICAL,8);
|
||||||
|
gtk_widget_set_name(editor->root,"person-factual-relation-editor");
|
||||||
GtkWidget *header=gtk_box_new(GTK_ORIENTATION_HORIZONTAL,8);
|
GtkWidget *header=gtk_box_new(GTK_ORIENTATION_HORIZONTAL,8);
|
||||||
GtkWidget *title=gtk_label_new("Relations factuelles");
|
GtkWidget *title=gtk_label_new("Relations factuelles");
|
||||||
gtk_widget_set_hexpand(title,TRUE);gtk_widget_set_halign(title,GTK_ALIGN_START);
|
gtk_widget_set_hexpand(title,TRUE);gtk_widget_set_halign(title,GTK_ALIGN_START);
|
||||||
|
|
|
||||||
197
src/views/person_ocr_projection_editor.c
Normal file
197
src/views/person_ocr_projection_editor.c
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
#include "views/person_ocr_projection_editor.h"
|
||||||
|
#include "core/person_ocr_projection_mapping.h"
|
||||||
|
#include "models/identity_traceability.h"
|
||||||
|
typedef struct {
|
||||||
|
GtkCheckButton *selected;
|
||||||
|
GtkDropDown *target;
|
||||||
|
GtkStringList *targets;
|
||||||
|
GtkDropDown *strategy;
|
||||||
|
IdentityOcrRun *run;
|
||||||
|
IdentityFieldObservation *field;
|
||||||
|
} Row;
|
||||||
|
struct PersonOcrProjectionEditor {
|
||||||
|
GtkWidget *root;
|
||||||
|
GtkLabel *title;
|
||||||
|
GtkBox *rows;
|
||||||
|
GtkLabel *empty;
|
||||||
|
GPtrArray *items;
|
||||||
|
};
|
||||||
|
static void row_free(Row *r) {
|
||||||
|
if (!r)
|
||||||
|
return;
|
||||||
|
g_clear_object(&r->targets);
|
||||||
|
g_free(r);
|
||||||
|
}
|
||||||
|
static const char *status_text(IdentityReviewStatus s) {
|
||||||
|
return s == IDENTITY_REVIEW_ACCEPTED ? "accepted"
|
||||||
|
: s == IDENTITY_REVIEW_MODIFIED ? "modified"
|
||||||
|
: s == IDENTITY_REVIEW_REJECTED ? "rejected"
|
||||||
|
: s == IDENTITY_REVIEW_CONFLICT ? "conflict"
|
||||||
|
: "proposed";
|
||||||
|
}
|
||||||
|
static void clear(PersonOcrProjectionEditor *e) {
|
||||||
|
GtkWidget *c = gtk_widget_get_first_child(GTK_WIDGET(e->rows));
|
||||||
|
while (c) {
|
||||||
|
GtkWidget *n = gtk_widget_get_next_sibling(c);
|
||||||
|
gtk_box_remove(e->rows, c);
|
||||||
|
c = n;
|
||||||
|
}
|
||||||
|
g_ptr_array_set_size(e->items, 0);
|
||||||
|
}
|
||||||
|
PersonOcrProjectionEditor *person_ocr_projection_editor_new(void) {
|
||||||
|
PersonOcrProjectionEditor *e = g_new0(PersonOcrProjectionEditor, 1);
|
||||||
|
e->root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||||
|
gtk_widget_set_name(e->root, "person-ocr-projection-editor");
|
||||||
|
e->title = GTK_LABEL(gtk_label_new(
|
||||||
|
"Appliquer des données OCR à la personne — facultatif"));
|
||||||
|
gtk_box_append(GTK_BOX(e->root), GTK_WIDGET(e->title));
|
||||||
|
e->empty = GTK_LABEL(gtk_label_new(
|
||||||
|
"Aucune donnée OCR confirmée ne peut être appliquée à cette personne.\n"
|
||||||
|
"La transcription OCR reste enregistrée dans la preuve."));
|
||||||
|
gtk_label_set_wrap(e->empty, TRUE);
|
||||||
|
gtk_box_append(GTK_BOX(e->root), GTK_WIDGET(e->empty));
|
||||||
|
e->rows = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 8));
|
||||||
|
gtk_box_append(GTK_BOX(e->root), GTK_WIDGET(e->rows));
|
||||||
|
e->items = g_ptr_array_new_with_free_func((GDestroyNotify)row_free);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
GtkWidget *
|
||||||
|
person_ocr_projection_editor_get_widget(PersonOcrProjectionEditor *e) {
|
||||||
|
return e ? e->root : NULL;
|
||||||
|
}
|
||||||
|
void person_ocr_projection_editor_set_runs(PersonOcrProjectionEditor *e,
|
||||||
|
const GPtrArray *runs) {
|
||||||
|
if (!e)
|
||||||
|
return;
|
||||||
|
clear(e);
|
||||||
|
for (guint i = 0; runs && i < runs->len; i++) {
|
||||||
|
IdentityOcrRun *run = g_ptr_array_index((GPtrArray *)runs, i);
|
||||||
|
const GPtrArray *fields = identity_ocr_run_get_fields(run);
|
||||||
|
for (guint j = 0; fields && j < fields->len; j++) {
|
||||||
|
IdentityFieldObservation *f = g_ptr_array_index((GPtrArray *)fields, j);
|
||||||
|
const PersonOcrProjectionMapping *m = person_ocr_projection_mapping_for(
|
||||||
|
identity_field_observation_get_code(f));
|
||||||
|
const char *q = identity_field_observation_get_value_quality(f),
|
||||||
|
*v = identity_field_observation_get_confirmed_value(f);
|
||||||
|
const char *s = status_text(identity_field_observation_get_status(f));
|
||||||
|
if (!m || !identity_traceability_field_is_projectable(
|
||||||
|
s, q,
|
||||||
|
identity_field_observation_is_human_confirmed(f)
|
||||||
|
? "human_confirmed"
|
||||||
|
: "unconfirmed",
|
||||||
|
v))
|
||||||
|
continue;
|
||||||
|
Row *r = g_new0(Row, 1);
|
||||||
|
r->run = run;
|
||||||
|
r->field = f;
|
||||||
|
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||||
|
char *text = g_strdup_printf(
|
||||||
|
"%s%s\nBrute : %s\nNormalisée : %s\nCorrigée : %s\nConfirmée : "
|
||||||
|
"%s\nValeur actuelle : —\nRésultat prévisualisé : %s\nQualité : %s — "
|
||||||
|
"statut : %s\nPreuve : %s — OcrRun : %s",
|
||||||
|
m->label, g_strcmp0(q, "partial") == 0 ? " — Valeur partielle" : "",
|
||||||
|
identity_field_observation_get_raw_value(f)
|
||||||
|
? identity_field_observation_get_raw_value(f)
|
||||||
|
: "—",
|
||||||
|
identity_field_observation_get_normalized_value(f)
|
||||||
|
? identity_field_observation_get_normalized_value(f)
|
||||||
|
: "—",
|
||||||
|
identity_field_observation_get_corrected_value(f)
|
||||||
|
? identity_field_observation_get_corrected_value(f)
|
||||||
|
: "—",
|
||||||
|
v, v, q, s, identity_ocr_run_get_evidence_id(run),
|
||||||
|
identity_ocr_run_get_identifier(run));
|
||||||
|
r->selected = GTK_CHECK_BUTTON(gtk_check_button_new_with_label(text));
|
||||||
|
gtk_widget_set_name(GTK_WIDGET(r->selected), "projection-select");
|
||||||
|
gtk_box_append(GTK_BOX(box), GTK_WIDGET(r->selected));
|
||||||
|
g_free(text);
|
||||||
|
r->targets = gtk_string_list_new(NULL);
|
||||||
|
gtk_string_list_append(r->targets, "Choisir un champ cible");
|
||||||
|
gtk_string_list_append(r->targets, m->label);
|
||||||
|
r->target = GTK_DROP_DOWN(
|
||||||
|
gtk_drop_down_new(G_LIST_MODEL(g_object_ref(r->targets)), NULL));
|
||||||
|
gtk_widget_set_name(GTK_WIDGET(r->target), "projection-target");
|
||||||
|
gtk_box_append(GTK_BOX(box), GTK_WIDGET(r->target));
|
||||||
|
static const char *strategies[] = {"Conserver la valeur actuelle",
|
||||||
|
"Renseigner si vide",
|
||||||
|
"Remplacer explicitement", NULL};
|
||||||
|
r->strategy = GTK_DROP_DOWN(gtk_drop_down_new_from_strings(strategies));
|
||||||
|
gtk_widget_set_name(GTK_WIDGET(r->strategy), "projection-strategy");
|
||||||
|
gtk_box_append(GTK_BOX(box), GTK_WIDGET(r->strategy));
|
||||||
|
gtk_box_append(e->rows, box);
|
||||||
|
g_ptr_array_add(e->items, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gtk_widget_set_visible(GTK_WIDGET(e->title), e->items->len > 0);
|
||||||
|
gtk_widget_set_visible(GTK_WIDGET(e->empty), e->items->len == 0);
|
||||||
|
}
|
||||||
|
gboolean person_ocr_projection_editor_collect(PersonOcrProjectionEditor *e,
|
||||||
|
GPtrArray **out, GError **error) {
|
||||||
|
if (out)
|
||||||
|
*out = NULL;
|
||||||
|
if (!e || !out)
|
||||||
|
return FALSE;
|
||||||
|
GPtrArray *a = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)person_ocr_field_projection_free);
|
||||||
|
for (guint i = 0; i < e->items->len; i++) {
|
||||||
|
Row *r = g_ptr_array_index(e->items, i);
|
||||||
|
if (!gtk_check_button_get_active(r->selected))
|
||||||
|
continue;
|
||||||
|
guint target = gtk_drop_down_get_selected(r->target),
|
||||||
|
strategy = gtk_drop_down_get_selected(r->strategy);
|
||||||
|
if (target != 1 || strategy == 0) {
|
||||||
|
g_set_error_literal(
|
||||||
|
error, g_quark_from_static_string("person-ocr-projection-editor"), 1,
|
||||||
|
"Choisissez un champ cible et une stratégie d’application.");
|
||||||
|
g_ptr_array_unref(a);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
const PersonOcrProjectionMapping *m = person_ocr_projection_mapping_for(
|
||||||
|
identity_field_observation_get_code(r->field));
|
||||||
|
PersonOcrFieldProjection *p = person_ocr_field_projection_new(
|
||||||
|
identity_ocr_run_get_evidence_id(r->run),
|
||||||
|
identity_ocr_run_get_identifier(r->run),
|
||||||
|
identity_field_observation_get_identifier(r->field),
|
||||||
|
identity_field_observation_get_code(r->field),
|
||||||
|
identity_field_observation_get_confirmed_value(r->field),
|
||||||
|
identity_field_observation_get_value_quality(r->field),
|
||||||
|
status_text(identity_field_observation_get_status(r->field)),
|
||||||
|
m->person_field, NULL,
|
||||||
|
strategy == 1 ? PERSON_OCR_FILL_EMPTY : PERSON_OCR_REPLACE_EXISTING,
|
||||||
|
TRUE);
|
||||||
|
if (!p) {
|
||||||
|
g_set_error_literal(
|
||||||
|
error, g_quark_from_static_string("person-ocr-projection-editor"), 2,
|
||||||
|
"La projection préparée est invalide.");
|
||||||
|
g_ptr_array_unref(a);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
g_ptr_array_add(a, p);
|
||||||
|
}
|
||||||
|
*out = a;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
void person_ocr_projection_editor_append_summary(PersonOcrProjectionEditor *e,
|
||||||
|
GString *summary) {
|
||||||
|
if (!e || !summary)
|
||||||
|
return;
|
||||||
|
GPtrArray *items = NULL;
|
||||||
|
g_string_append(summary, "\n\nValeurs OCR choisies pour projection");
|
||||||
|
if (!person_ocr_projection_editor_collect(e, &items, NULL))
|
||||||
|
return;
|
||||||
|
if (items->len == 0)
|
||||||
|
g_string_append(summary, "\nAucune valeur choisie.");
|
||||||
|
for (guint i = 0; i < items->len; i++) {
|
||||||
|
PersonOcrFieldProjection *item = g_ptr_array_index(items, i);
|
||||||
|
g_string_append_printf(summary, "\n• %s : %s",
|
||||||
|
person_ocr_field_projection_get_person_field(item),
|
||||||
|
person_ocr_field_projection_get_confirmed_value(item));
|
||||||
|
}
|
||||||
|
g_ptr_array_unref(items);
|
||||||
|
}
|
||||||
|
void person_ocr_projection_editor_free(PersonOcrProjectionEditor *e) {
|
||||||
|
if (!e)
|
||||||
|
return;
|
||||||
|
g_ptr_array_unref(e->items);
|
||||||
|
g_free(e);
|
||||||
|
}
|
||||||
|
|
@ -42,6 +42,7 @@ struct EntityDetailsPanel
|
||||||
GtkWidget *person_evidence_button;
|
GtkWidget *person_evidence_button;
|
||||||
GtkWidget *person_evidence_summary_label;
|
GtkWidget *person_evidence_summary_label;
|
||||||
GtkWidget *person_factual_relations_box;
|
GtkWidget *person_factual_relations_box;
|
||||||
|
GtkWidget *person_profile_fields_box;
|
||||||
GtkWidget *entity_evidence_box;
|
GtkWidget *entity_evidence_box;
|
||||||
GtkWidget *evidence_buttons_box;
|
GtkWidget *evidence_buttons_box;
|
||||||
|
|
||||||
|
|
@ -889,6 +890,8 @@ EntityDetailsPanel *entity_details_panel_new(void)
|
||||||
details_panel->person_evidence_button);
|
details_panel->person_evidence_button);
|
||||||
gtk_box_append(GTK_BOX(details_box), details_panel->person_role_box);
|
gtk_box_append(GTK_BOX(details_box), details_panel->person_role_box);
|
||||||
details_panel->person_factual_relations_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
details_panel->person_factual_relations_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||||
|
details_panel->person_profile_fields_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||||
|
gtk_box_append(GTK_BOX(details_box), details_panel->person_profile_fields_box);
|
||||||
gtk_box_append(GTK_BOX(details_box), details_panel->person_factual_relations_box);
|
gtk_box_append(GTK_BOX(details_box), details_panel->person_factual_relations_box);
|
||||||
gtk_box_append(GTK_BOX(details_box), details_panel->entity_evidence_box);
|
gtk_box_append(GTK_BOX(details_box), details_panel->entity_evidence_box);
|
||||||
g_signal_connect(details_panel->person_role_dropdown, "notify::selected",
|
g_signal_connect(details_panel->person_role_dropdown, "notify::selected",
|
||||||
|
|
@ -972,6 +975,41 @@ EntityDetailsPanel *entity_details_panel_new(void)
|
||||||
return details_panel;
|
return details_panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void entity_details_panel_set_person_profile_fields(EntityDetailsPanel *panel,
|
||||||
|
const GHashTable *fields)
|
||||||
|
{
|
||||||
|
static const struct { const char *code; const char *label; } rows[] = {
|
||||||
|
{"declared_name", "Nom déclaré"}, {"surname", "Nom"},
|
||||||
|
{"given_names", "Prénoms"}, {"birth_date", "Date de naissance"},
|
||||||
|
{"birth_place", "Lieu de naissance"}, {"nationality", "Nationalité"},
|
||||||
|
{"sex_as_printed", "Sexe imprimé"},
|
||||||
|
{"address_as_printed", "Adresse imprimée"}
|
||||||
|
};
|
||||||
|
if (panel == NULL || panel->person_profile_fields_box == NULL) return;
|
||||||
|
GtkWidget *child = NULL;
|
||||||
|
while ((child = gtk_widget_get_first_child(panel->person_profile_fields_box)))
|
||||||
|
gtk_box_remove(GTK_BOX(panel->person_profile_fields_box), child);
|
||||||
|
if (fields == NULL || g_hash_table_size((GHashTable *)fields) == 0) {
|
||||||
|
gtk_widget_set_visible(panel->person_profile_fields_box, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GtkWidget *title = gtk_label_new("Valeurs confirmées du profil");
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(title), 0.0F);
|
||||||
|
gtk_box_append(GTK_BOX(panel->person_profile_fields_box), title);
|
||||||
|
for (guint i = 0; i < G_N_ELEMENTS(rows); i++) {
|
||||||
|
const char *value = g_hash_table_lookup((GHashTable *)fields, rows[i].code);
|
||||||
|
if (value == NULL) continue;
|
||||||
|
char *text = g_strdup_printf("%s : %s", rows[i].label, value);
|
||||||
|
GtkWidget *label = gtk_label_new(text);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(label), 0.0F);
|
||||||
|
gtk_label_set_wrap(GTK_LABEL(label), TRUE);
|
||||||
|
gtk_label_set_selectable(GTK_LABEL(label), TRUE);
|
||||||
|
gtk_box_append(GTK_BOX(panel->person_profile_fields_box), label);
|
||||||
|
g_free(text);
|
||||||
|
}
|
||||||
|
gtk_widget_set_visible(panel->person_profile_fields_box, TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
GtkWidget *entity_details_panel_get_widget(
|
GtkWidget *entity_details_panel_get_widget(
|
||||||
const EntityDetailsPanel *details_panel
|
const EntityDetailsPanel *details_panel
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3914,8 +3914,8 @@ void workspace_set_entity_selected_callback(Workspace *workspace,
|
||||||
void workspace_set_person_evidences(Workspace *workspace, const GPtrArray *records) {
|
void workspace_set_person_evidences(Workspace *workspace, const GPtrArray *records) {
|
||||||
if (workspace) entity_details_panel_set_person_evidences(workspace->entity_details_panel, records);
|
if (workspace) entity_details_panel_set_person_evidences(workspace->entity_details_panel, records);
|
||||||
}
|
}
|
||||||
void workspace_set_person_factual_relations(Workspace *workspace, const GPtrArray *relations, const GPtrArray *evidence_records) {
|
void workspace_set_person_factual_relations(Workspace *workspace, const GPtrArray *relations, const GPtrArray *evidence_records,const GHashTable *profile_fields) {
|
||||||
if (workspace) entity_details_panel_set_person_factual_relations(workspace->entity_details_panel, relations, evidence_records);
|
if (workspace) { entity_details_panel_set_person_factual_relations(workspace->entity_details_panel, relations, evidence_records);entity_details_panel_set_person_profile_fields(workspace->entity_details_panel,profile_fields); }
|
||||||
}
|
}
|
||||||
|
|
||||||
void workspace_reset_graph_layout(
|
void workspace_reset_graph_layout(
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,37 @@ static GtkWidget *find_named(GtkWidget *widget, const char *name)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static guint count_label(GtkWidget *widget, const char *text)
|
||||||
|
{
|
||||||
|
guint count = GTK_IS_LABEL(widget) &&
|
||||||
|
g_strcmp0(gtk_label_get_text(GTK_LABEL(widget)), text) == 0 ? 1 : 0;
|
||||||
|
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||||
|
child != NULL; child = gtk_widget_get_next_sibling(child))
|
||||||
|
count += count_label(child, text);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void assert_assistant_steps(GtkWidget *dialog)
|
||||||
|
{
|
||||||
|
static const char *const names[] = {"person", "roles", "evidence",
|
||||||
|
"identity-ocr", "ocr-projection", "factual-relations", "summary"};
|
||||||
|
static const char *const titles[] = {"1 — Personne", "2 — Rôles",
|
||||||
|
"3 — Preuves", "4 — OCR identité", "5 — Projection OCR",
|
||||||
|
"6 — Relations factuelles", "7 — Confirmation"};
|
||||||
|
GtkStack *stack = GTK_STACK(find_named(dialog,
|
||||||
|
"create-person-assistant-stack"));
|
||||||
|
g_assert_nonnull(stack);
|
||||||
|
GtkSelectionModel *selection = gtk_stack_get_pages(stack);
|
||||||
|
GListModel *pages = G_LIST_MODEL(selection);
|
||||||
|
g_assert_cmpuint(g_list_model_get_n_items(pages), ==, G_N_ELEMENTS(names));
|
||||||
|
for (guint i = 0; i < G_N_ELEMENTS(names); i++) {
|
||||||
|
GtkStackPage *page = g_list_model_get_item(pages, i);
|
||||||
|
g_assert_cmpstr(gtk_stack_page_get_name(page), ==, names[i]);
|
||||||
|
g_assert_cmpstr(gtk_stack_page_get_title(page), ==, titles[i]);
|
||||||
|
g_object_unref(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static gboolean frame_seen(GtkWidget *widget, GdkFrameClock *clock,
|
static gboolean frame_seen(GtkWidget *widget, GdkFrameClock *clock,
|
||||||
gpointer data)
|
gpointer data)
|
||||||
{
|
{
|
||||||
|
|
@ -163,6 +194,7 @@ static gboolean click_cancel(gpointer data)
|
||||||
context->main_window);
|
context->main_window);
|
||||||
g_assert_true(gtk_window_get_modal(dialog));
|
g_assert_true(gtk_window_get_modal(dialog));
|
||||||
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
|
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
|
||||||
|
assert_assistant_steps(GTK_WIDGET(dialog));
|
||||||
gtk_window_set_default_size(dialog, 760, 560);
|
gtk_window_set_default_size(dialog, 760, 560);
|
||||||
wait_for_frame(GTK_WIDGET(dialog));
|
wait_for_frame(GTK_WIDGET(dialog));
|
||||||
designation = find_entry(GTK_WIDGET(dialog),
|
designation = find_entry(GTK_WIDGET(dialog),
|
||||||
|
|
@ -221,12 +253,35 @@ static gboolean click_cancel(gpointer data)
|
||||||
g_signal_emit_by_name(button, "clicked");
|
g_signal_emit_by_name(button, "clicked");
|
||||||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||||
"L’OCR produit des propositions à vérifier"));
|
"L’OCR produit des propositions à vérifier"));
|
||||||
|
g_assert_nonnull(find_named(GTK_WIDGET(dialog),
|
||||||
|
"create-person-ocr-transcription-stack"));
|
||||||
|
g_signal_emit_by_name(button, "clicked");
|
||||||
|
GtkStack *stack = GTK_STACK(find_named(GTK_WIDGET(dialog),
|
||||||
|
"create-person-assistant-stack"));
|
||||||
|
g_assert_true(gtk_widget_is_ancestor(
|
||||||
|
find_named(GTK_WIDGET(dialog), "person-ocr-projection-editor"),
|
||||||
|
gtk_stack_get_visible_child(stack)));
|
||||||
|
const char *empty_projection =
|
||||||
|
"Aucune donnée OCR confirmée ne peut être appliquée à cette personne.\n"
|
||||||
|
"La transcription OCR reste enregistrée dans la preuve.";
|
||||||
|
g_assert_cmpuint(count_label(GTK_WIDGET(dialog), empty_projection), ==, 1);
|
||||||
|
g_assert_cmpuint(count_label(GTK_WIDGET(dialog),
|
||||||
|
"Valeurs OCR confirmées à appliquer explicitement"), ==, 0);
|
||||||
g_signal_emit_by_name(button, "clicked");
|
g_signal_emit_by_name(button, "clicked");
|
||||||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||||
"Relations factuelles"));
|
"Relations factuelles"));
|
||||||
|
g_assert_true(gtk_widget_is_ancestor(
|
||||||
|
find_named(GTK_WIDGET(dialog), "person-factual-relation-editor"),
|
||||||
|
gtk_stack_get_visible_child(stack)));
|
||||||
g_signal_emit_by_name(button, "clicked");
|
g_signal_emit_by_name(button, "clicked");
|
||||||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||||
"Désignation : SPECIMEN"));
|
"Désignation : SPECIMEN"));
|
||||||
|
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||||
|
"Correction OCR enregistrée"));
|
||||||
|
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||||
|
"Valeurs OCR choisies pour projection"));
|
||||||
|
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||||
|
"Relations factuelles préparées"));
|
||||||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||||
"SPECIMEN-recherche.png"));
|
"SPECIMEN-recherche.png"));
|
||||||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||||
|
|
|
||||||
|
|
@ -570,8 +570,15 @@ static gboolean drive(gpointer data)
|
||||||
context->confirmation_height=gtk_widget_get_height(
|
context->confirmation_height=gtk_widget_get_height(
|
||||||
GTK_WIDGET(context->dialog));
|
GTK_WIDGET(context->dialog));
|
||||||
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
|
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
|
||||||
|
g_assert_nonnull(find_label(root,
|
||||||
|
"Appliquer des données OCR à la personne — facultatif"));
|
||||||
|
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
|
||||||
g_assert_nonnull(find_label(root,"Relations factuelles"));
|
g_assert_nonnull(find_label(root,"Relations factuelles"));
|
||||||
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
|
g_signal_emit_by_name(find_button(root,"Suivant"),"clicked");
|
||||||
|
g_assert_nonnull(find_label(root,"Correction OCR enregistrée"));
|
||||||
|
g_assert_nonnull(find_label(root,
|
||||||
|
"Valeurs OCR choisies pour projection"));
|
||||||
|
g_assert_nonnull(find_label(root,"Relations factuelles préparées"));
|
||||||
g_assert_nonnull(find_label(root,"page 2"));
|
g_assert_nonnull(find_label(root,"page 2"));
|
||||||
g_assert_nonnull(find_label(root,"PAGE 2 VALEUR FINALE"));
|
g_assert_nonnull(find_label(root,"PAGE 2 VALEUR FINALE"));
|
||||||
g_assert_nonnull(find_label(root,"authenticité n’est pas établie"));
|
g_assert_nonnull(find_label(root,"authenticité n’est pas établie"));
|
||||||
|
|
|
||||||
|
|
@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void)
|
||||||
"FROM investigation;"
|
"FROM investigation;"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(strcmp(schema_version, "18") == 0);
|
assert(strcmp(schema_version, "19") == 0);
|
||||||
test_database_assert_table_exists(database, "person_role_assignments");
|
test_database_assert_table_exists(database, "person_role_assignments");
|
||||||
test_database_assert_table_exists(database, "bank_account_entities");
|
test_database_assert_table_exists(database, "bank_account_entities");
|
||||||
test_database_assert_table_exists(database, "relation_types");
|
test_database_assert_table_exists(database, "relation_types");
|
||||||
|
|
@ -1001,7 +1001,7 @@ static void test_database_migrate_v1_to_v2(void)
|
||||||
assert(
|
assert(
|
||||||
strcmp(
|
strcmp(
|
||||||
schema_version,
|
schema_version,
|
||||||
"18"
|
"19"
|
||||||
) == 0
|
) == 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1430,6 +1430,8 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
|
||||||
"INSERT INTO person_roles(entity_id,role,updated_at) VALUES("
|
"INSERT INTO person_roles(entity_id,role,updated_at) VALUES("
|
||||||
"'20000000-0000-4000-8000-000000000014','alleged_scammer',"
|
"'20000000-0000-4000-8000-000000000014','alleged_scammer',"
|
||||||
"'2026-07-28T08:00:00Z');"
|
"'2026-07-28T08:00:00Z');"
|
||||||
|
"DROP TABLE person_ocr_field_projections;"
|
||||||
|
"DROP TABLE person_profile_fields;"
|
||||||
"DROP TABLE person_identification_assessments;"
|
"DROP TABLE person_identification_assessments;"
|
||||||
"DROP TABLE person_evidence_factual_relations;"
|
"DROP TABLE person_evidence_factual_relations;"
|
||||||
"DROP TABLE document_authenticity_assessments;"
|
"DROP TABLE document_authenticity_assessments;"
|
||||||
|
|
@ -1464,7 +1466,7 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
|
||||||
legacy_sources = test_database_read_single_text(sqlite_database,
|
legacy_sources = test_database_read_single_text(sqlite_database,
|
||||||
"SELECT COUNT(*) FROM preuve_entite_sources "
|
"SELECT COUNT(*) FROM preuve_entite_sources "
|
||||||
"WHERE source_kind='legacy_manual';");
|
"WHERE source_kind='legacy_manual';");
|
||||||
assert(strcmp(version, "18") == 0);
|
assert(strcmp(version, "19") == 0);
|
||||||
assert(strcmp(legacy_sources, "1") == 0);
|
assert(strcmp(legacy_sources, "1") == 0);
|
||||||
char *legacy_role = test_database_read_single_text(sqlite_database,
|
char *legacy_role = test_database_read_single_text(sqlite_database,
|
||||||
"SELECT role_code || ':' || provenance_kind "
|
"SELECT role_code || ':' || provenance_kind "
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@ static void test_migrate_v17_preserves_ocr(void)
|
||||||
Fixture f=fixture_new();database_close(f.database);f.database=NULL;
|
Fixture f=fixture_new();database_close(f.database);f.database=NULL;
|
||||||
sqlite3*d=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
|
sqlite3*d=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
|
||||||
exec_ok(d,"PRAGMA foreign_keys=OFF;"
|
exec_ok(d,"PRAGMA foreign_keys=OFF;"
|
||||||
|
"DROP TABLE person_ocr_field_projections;DROP TABLE person_profile_fields;"
|
||||||
"DROP TABLE person_identification_assessments;"
|
"DROP TABLE person_identification_assessments;"
|
||||||
"DROP TABLE person_evidence_factual_relations;"
|
"DROP TABLE person_evidence_factual_relations;"
|
||||||
"DROP TABLE document_authenticity_assessments;"
|
"DROP TABLE document_authenticity_assessments;"
|
||||||
|
|
@ -257,7 +258,7 @@ static void test_migrate_v17_preserves_ocr(void)
|
||||||
char*version=scalar(d,"SELECT value FROM metadata WHERE key='schema_version';");
|
char*version=scalar(d,"SELECT value FROM metadata WHERE key='schema_version';");
|
||||||
char*values=scalar(d,"SELECT raw_value||':'||normalized_value||':'||corrected_value"
|
char*values=scalar(d,"SELECT raw_value||':'||normalized_value||':'||corrected_value"
|
||||||
"||':'||confirmation_state FROM identity_field_observations WHERE id='" FIELD "';");
|
"||':'||confirmation_state FROM identity_field_observations WHERE id='" FIELD "';");
|
||||||
g_assert_cmpstr(version,==,"18");
|
g_assert_cmpstr(version,==,"19");
|
||||||
g_assert_cmpstr(values,==,"BRUT SPECIMEN:BRUT SPECIMEN:CORRIGÉ SPECIMEN:unconfirmed");
|
g_assert_cmpstr(values,==,"BRUT SPECIMEN:BRUT SPECIMEN:CORRIGÉ SPECIMEN:unconfirmed");
|
||||||
sqlite3_close(d);g_free(version);g_free(values);
|
sqlite3_close(d);g_free(version);g_free(values);
|
||||||
f.database=database_open(f.path);g_assert_true(database_migrate_to_latest(f.database));
|
f.database=database_open(f.path);g_assert_true(database_migrate_to_latest(f.database));
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ static void assert_empty_after_reopen(const char *database_path)
|
||||||
"entites", "preuves", "preuve_entites", "preuve_entite_sources",
|
"entites", "preuves", "preuve_entites", "preuve_entite_sources",
|
||||||
"person_role_assignments", "identity_ocr_runs",
|
"person_role_assignments", "identity_ocr_runs",
|
||||||
"identity_document_observations", "identity_field_observations",
|
"identity_document_observations", "identity_field_observations",
|
||||||
"person_evidence_factual_relations"
|
"person_evidence_factual_relations", "person_profile_fields",
|
||||||
|
"person_ocr_field_projections"
|
||||||
};
|
};
|
||||||
Database *database = database_open(database_path);
|
Database *database = database_open(database_path);
|
||||||
g_assert_nonnull(database);
|
g_assert_nonnull(database);
|
||||||
|
|
@ -102,6 +103,7 @@ static void test_failure_matrix(void)
|
||||||
{PERSON_CREATION_FAILURE_CREATE_SOURCE,1},
|
{PERSON_CREATION_FAILURE_CREATE_SOURCE,1},
|
||||||
{PERSON_CREATION_FAILURE_CREATE_SOURCE,2},
|
{PERSON_CREATION_FAILURE_CREATE_SOURCE,2},
|
||||||
{PERSON_CREATION_FAILURE_INSERT_FACTUAL_RELATION,0},
|
{PERSON_CREATION_FAILURE_INSERT_FACTUAL_RELATION,0},
|
||||||
|
{PERSON_CREATION_FAILURE_APPLY_OCR_PROJECTION,0},
|
||||||
{PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,0},
|
{PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,0},
|
||||||
{PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,0},
|
{PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,0},
|
||||||
{PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,0},
|
{PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,0},
|
||||||
|
|
@ -152,6 +154,7 @@ static void test_failure_matrix(void)
|
||||||
identity_field_observation_new("surname", "SPECIMEN",
|
identity_field_observation_new("surname", "SPECIMEN",
|
||||||
90, NULL, i);
|
90, NULL, i);
|
||||||
identity_field_observation_accept(field);
|
identity_field_observation_accept(field);
|
||||||
|
identity_field_observation_confirm(field,"SPECIMEN");
|
||||||
identity_ocr_run_add_field(run, field);
|
identity_ocr_run_add_field(run, field);
|
||||||
g_ptr_array_add(runs, run);
|
g_ptr_array_add(runs, run);
|
||||||
PersonRoleAssignmentInput role = {
|
PersonRoleAssignmentInput role = {
|
||||||
|
|
@ -180,12 +183,23 @@ static void test_failure_matrix(void)
|
||||||
};
|
};
|
||||||
GPtrArray *factual_relations = g_ptr_array_new();
|
GPtrArray *factual_relations = g_ptr_array_new();
|
||||||
g_ptr_array_add(factual_relations, &factual_relation);
|
g_ptr_array_add(factual_relations, &factual_relation);
|
||||||
|
IdentityOcrRun *projection_run=g_ptr_array_index(runs,0);
|
||||||
|
IdentityFieldObservation *projection_field=g_ptr_array_index(
|
||||||
|
(GPtrArray*)identity_ocr_run_get_fields(projection_run),0);
|
||||||
|
PersonOcrFieldProjection *projection=person_ocr_field_projection_new(
|
||||||
|
identity_ocr_run_get_evidence_id(projection_run),
|
||||||
|
identity_ocr_run_get_identifier(projection_run),
|
||||||
|
identity_field_observation_get_identifier(projection_field),
|
||||||
|
"surname","SPECIMEN","complete","accepted","surname",NULL,
|
||||||
|
PERSON_OCR_FILL_EMPTY,TRUE);
|
||||||
|
GPtrArray *projections=g_ptr_array_new();g_ptr_array_add(projections,projection);
|
||||||
PersonCreationCoordinatorOptions options = {
|
PersonCreationCoordinatorOptions options = {
|
||||||
.failure_point = cases[scenario].point,
|
.failure_point = cases[scenario].point,
|
||||||
.failure_occurrence = cases[scenario].occurrence,
|
.failure_occurrence = cases[scenario].occurrence,
|
||||||
.inject_compensation_failure =
|
.inject_compensation_failure =
|
||||||
scenario == G_N_ELEMENTS(cases) - 1,
|
scenario == G_N_ELEMENTS(cases) - 1,
|
||||||
.factual_relations = factual_relations
|
.factual_relations = factual_relations
|
||||||
|
,.ocr_projections = projections
|
||||||
};
|
};
|
||||||
char *ocr_parent=g_build_filename(root,"02_Preuves_Traitees",
|
char *ocr_parent=g_build_filename(root,"02_Preuves_Traitees",
|
||||||
"OCR",NULL);
|
"OCR",NULL);
|
||||||
|
|
@ -238,6 +252,7 @@ static void test_failure_matrix(void)
|
||||||
}
|
}
|
||||||
g_ptr_array_unref(roles);
|
g_ptr_array_unref(roles);
|
||||||
g_ptr_array_unref(factual_relations);
|
g_ptr_array_unref(factual_relations);
|
||||||
|
g_ptr_array_unref(projections);person_ocr_field_projection_free(projection);
|
||||||
g_ptr_array_unref(runs);
|
g_ptr_array_unref(runs);
|
||||||
person_evidence_selection_free(selection);
|
person_evidence_selection_free(selection);
|
||||||
g_ptr_array_unref(prepared);
|
g_ptr_array_unref(prepared);
|
||||||
|
|
|
||||||
251
tests/test_person_ocr_projection.c
Normal file
251
tests/test_person_ocr_projection.c
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
#include "core/person_ocr_projection_mapping.h"
|
||||||
|
#include "core/person_ocr_projection_service.h"
|
||||||
|
#include "dao/person_ocr_projection_dao.h"
|
||||||
|
#include "database/database.h"
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#define PERSON "20000000-0000-4000-8000-000000000019"
|
||||||
|
#define EVIDENCE "10000000-0000-4000-8000-000000000019"
|
||||||
|
#define RUN "30000000-0000-4000-8000-000000000019"
|
||||||
|
#define DOC "40000000-0000-4000-8000-000000000019"
|
||||||
|
#define FIELD "50000000-0000-4000-8000-000000000019"
|
||||||
|
#define FIELD2 "50000000-0000-4000-8000-000000000020"
|
||||||
|
#define AT "2026-08-01T10:00:00Z"
|
||||||
|
typedef struct {
|
||||||
|
char *dir, *path;
|
||||||
|
Database *d;
|
||||||
|
} Fixture;
|
||||||
|
static void exec_ok(sqlite3 *d, const char *s) {
|
||||||
|
char *m = NULL;
|
||||||
|
int rc = sqlite3_exec(d, s, NULL, NULL, &m);
|
||||||
|
if (rc != SQLITE_OK)
|
||||||
|
g_test_message("SQLite: %s", m);
|
||||||
|
g_assert_cmpint(rc, ==, SQLITE_OK);
|
||||||
|
sqlite3_free(m);
|
||||||
|
}
|
||||||
|
static Fixture fixture(void) {
|
||||||
|
Fixture f = {0};
|
||||||
|
f.dir = g_dir_make_tmp("labfy-projection-XXXXXX", NULL);
|
||||||
|
f.path = g_build_filename(f.dir, "SPECIMEN.sqlite", NULL);
|
||||||
|
g_assert_true(database_initialize(f.path, "SPECIMEN", f.dir));
|
||||||
|
sqlite3 *s = NULL;
|
||||||
|
sqlite3_open(f.path, &s);
|
||||||
|
exec_ok(
|
||||||
|
s,
|
||||||
|
"INSERT INTO "
|
||||||
|
"preuves(id,name,relative_path,type_id,size_bytes,sha256,imported_at,"
|
||||||
|
"updated_at,status,locked,original_name) VALUES('" EVIDENCE
|
||||||
|
"','SPECIMEN','SPECIMEN',2,8,'"
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa','" AT
|
||||||
|
"','" AT "','active',0,'SPECIMEN');INSERT INTO "
|
||||||
|
"entites(id,type_id,valeur,label,confiance,created_at,updated_"
|
||||||
|
"at,status) VALUES('" PERSON
|
||||||
|
"',(SELECT id FROM types_entite WHERE code='person'),'PERSONNE "
|
||||||
|
"SPECIMEN','PERSONNE SPECIMEN',0,'" AT "','" AT
|
||||||
|
"','active');INSERT INTO "
|
||||||
|
"identity_ocr_runs(id,evidence_id,expected_sha256,page_number,document_"
|
||||||
|
"type,document_side,engine,requested_languages,available_languages,"
|
||||||
|
"parameters,preprocessing_profile,executed_at,status,text_relative_path,"
|
||||||
|
"text_sha256,tsv_relative_path,tsv_sha256) VALUES('" RUN "','" EVIDENCE
|
||||||
|
"','aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',1,'"
|
||||||
|
"identity_card','front','SPECIMEN','fra','fra','SPECIMEN','none','" AT
|
||||||
|
"','success','SPECIMEN.txt','"
|
||||||
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb','"
|
||||||
|
"SPECIMEN.tsv','"
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc');"
|
||||||
|
"INSERT INTO "
|
||||||
|
"identity_document_observations(id,person_id,evidence_id,ocr_run_id,"
|
||||||
|
"document_type,document_side,page_number,review_state,observed_at) "
|
||||||
|
"VALUES('" DOC "','" PERSON "','" EVIDENCE "','" RUN
|
||||||
|
"','identity_card','front',1,'accepted','" AT
|
||||||
|
"');INSERT INTO "
|
||||||
|
"identity_field_observations(id,observation_id,field_code,raw_value,"
|
||||||
|
"normalized_value,review_status,origin,evidence_id,ocr_run_id,page_"
|
||||||
|
"number,display_order,reviewed_at,confirmed_value,confirmation_state,"
|
||||||
|
"value_quality) VALUES('" FIELD "','" DOC
|
||||||
|
"','surname','BRUT','NORMALISÉ','accepted','ocr','" EVIDENCE "','" RUN
|
||||||
|
"',1,0,'" AT "','NOM SPECIMEN','human_confirmed','complete'),('" FIELD2
|
||||||
|
"','" DOC "','given_names','BRUT "
|
||||||
|
"PARTIEL','PARTIEL','modified','manual_override','" EVIDENCE
|
||||||
|
"','" RUN "',1,1,'" AT
|
||||||
|
"','PRÉNOM SPECIMEN','human_confirmed','partial');");
|
||||||
|
sqlite3_close(s);
|
||||||
|
f.d = database_open(f.path);
|
||||||
|
g_assert_true(database_migrate_to_latest(f.d));
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
static void done(Fixture *f) {
|
||||||
|
database_close(f->d);
|
||||||
|
g_remove(f->path);
|
||||||
|
g_rmdir(f->dir);
|
||||||
|
g_free(f->path);
|
||||||
|
g_free(f->dir);
|
||||||
|
}
|
||||||
|
static PersonOcrFieldProjection *
|
||||||
|
projection(const char *field, const char *code, const char *value,
|
||||||
|
const char *q, const char *target, const char *current,
|
||||||
|
PersonOcrProjectionStrategy strategy) {
|
||||||
|
return person_ocr_field_projection_new(
|
||||||
|
EVIDENCE, RUN, field, code, value, q,
|
||||||
|
g_strcmp0(q, "partial") == 0 ? "modified" : "accepted", target, current,
|
||||||
|
strategy, TRUE);
|
||||||
|
}
|
||||||
|
static void test_model_mapping(void) {
|
||||||
|
PersonOcrFieldProjection *p =
|
||||||
|
projection(FIELD, "surname", "NOM SPECIMEN", "complete", "surname", NULL,
|
||||||
|
PERSON_OCR_FILL_EMPTY);
|
||||||
|
g_assert_nonnull(p);
|
||||||
|
PersonOcrFieldProjection *c = person_ocr_field_projection_copy(p);
|
||||||
|
g_assert_cmpstr(person_ocr_field_projection_get_confirmed_value(c), ==,
|
||||||
|
"NOM SPECIMEN");
|
||||||
|
g_assert_true(
|
||||||
|
person_ocr_projection_mapping_is_compatible("surname", "surname"));
|
||||||
|
g_assert_false(person_ocr_projection_mapping_is_compatible("document_number",
|
||||||
|
"surname"));
|
||||||
|
g_assert_null(person_ocr_field_projection_new(
|
||||||
|
EVIDENCE, RUN, FIELD, "surname", "", "complete", "accepted", "surname",
|
||||||
|
NULL, PERSON_OCR_FILL_EMPTY, TRUE));
|
||||||
|
g_assert_null(person_ocr_field_projection_new(
|
||||||
|
EVIDENCE, RUN, FIELD, "surname", "NOM", NULL, "accepted", "surname",
|
||||||
|
NULL, PERSON_OCR_FILL_EMPTY, TRUE));
|
||||||
|
g_assert_null(person_ocr_field_projection_new(
|
||||||
|
EVIDENCE, RUN, FIELD, "surname", "NOM", "uncertain", "accepted",
|
||||||
|
"surname", NULL, PERSON_OCR_FILL_EMPTY, TRUE));
|
||||||
|
g_assert_null(person_ocr_field_projection_new(
|
||||||
|
EVIDENCE, RUN, FIELD, "surname", "NOM", "complete", "rejected",
|
||||||
|
"surname", NULL, PERSON_OCR_FILL_EMPTY, TRUE));
|
||||||
|
char invalid_utf8[] = {(char)0xff, 0};
|
||||||
|
g_assert_null(person_ocr_field_projection_new(
|
||||||
|
EVIDENCE, RUN, FIELD, "surname", invalid_utf8, "complete", "accepted",
|
||||||
|
"surname", NULL, PERSON_OCR_FILL_EMPTY, TRUE));
|
||||||
|
person_ocr_field_projection_free(c);
|
||||||
|
person_ocr_field_projection_free(p);
|
||||||
|
}
|
||||||
|
static void test_conflicts_and_foreign_sources(void) {
|
||||||
|
Fixture f = fixture();
|
||||||
|
GError *e = NULL;
|
||||||
|
sqlite3 *s = NULL;
|
||||||
|
database_close(f.d);
|
||||||
|
f.d = NULL;
|
||||||
|
sqlite3_open(f.path, &s);
|
||||||
|
exec_ok(s, "INSERT INTO person_profile_fields VALUES('" PERSON
|
||||||
|
"','surname','ANCIEN SPECIMEN','" AT "');");
|
||||||
|
sqlite3_close(s);
|
||||||
|
f.d = database_open(f.path);
|
||||||
|
GPtrArray *a = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)person_ocr_field_projection_free);
|
||||||
|
g_ptr_array_add(a, projection(FIELD, "surname", "NOM SPECIMEN", "complete",
|
||||||
|
"surname", "ANCIEN SPECIMEN",
|
||||||
|
PERSON_OCR_KEEP_EXISTING));
|
||||||
|
g_assert_true(person_ocr_projection_service_apply(f.d, PERSON, a, &e));
|
||||||
|
PersonOcrProjectionDao *dao = person_ocr_projection_dao_new(f.d);
|
||||||
|
char *value = person_ocr_projection_dao_get_value(dao, PERSON, "surname", &e);
|
||||||
|
g_assert_cmpstr(value, ==, "ANCIEN SPECIMEN");
|
||||||
|
g_free(value);
|
||||||
|
g_ptr_array_set_size(a, 0);
|
||||||
|
g_ptr_array_add(a, projection(FIELD, "surname", "NOM SPECIMEN", "complete",
|
||||||
|
"surname", "ANCIEN SPECIMEN",
|
||||||
|
PERSON_OCR_REPLACE_EXISTING));
|
||||||
|
g_assert_true(person_ocr_projection_service_apply(f.d, PERSON, a, &e));
|
||||||
|
GHashTable *fields = person_ocr_projection_dao_list_profile_fields(
|
||||||
|
dao, PERSON, &e);
|
||||||
|
g_assert_no_error(e);
|
||||||
|
g_assert_cmpstr(g_hash_table_lookup(fields, "surname"), ==, "NOM SPECIMEN");
|
||||||
|
g_hash_table_unref(fields);
|
||||||
|
g_ptr_array_set_size(a, 0);
|
||||||
|
g_ptr_array_add(a, person_ocr_field_projection_new(
|
||||||
|
EVIDENCE, "30000000-0000-4000-8000-000000000099",
|
||||||
|
FIELD, "surname", "NOM SPECIMEN", "complete",
|
||||||
|
"accepted", "surname", "NOM SPECIMEN",
|
||||||
|
PERSON_OCR_REPLACE_EXISTING, TRUE));
|
||||||
|
g_assert_false(person_ocr_projection_service_apply(f.d, PERSON, a, &e));
|
||||||
|
g_assert_nonnull(e);
|
||||||
|
g_clear_error(&e);
|
||||||
|
person_ocr_projection_dao_free(dao);
|
||||||
|
g_ptr_array_unref(a);
|
||||||
|
done(&f);
|
||||||
|
}
|
||||||
|
static void test_mid_series_rollback(void) {
|
||||||
|
Fixture f = fixture();
|
||||||
|
GError *e = NULL;
|
||||||
|
GPtrArray *a = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)person_ocr_field_projection_free);
|
||||||
|
g_ptr_array_add(a, projection(FIELD2, "given_names", "PRÉNOM SPECIMEN",
|
||||||
|
"partial", "given_names", NULL,
|
||||||
|
PERSON_OCR_FILL_EMPTY));
|
||||||
|
g_ptr_array_add(a, person_ocr_field_projection_new(
|
||||||
|
EVIDENCE, RUN, FIELD, "surname", "VALEUR OBSOLÈTE",
|
||||||
|
"complete", "accepted", "surname", NULL,
|
||||||
|
PERSON_OCR_FILL_EMPTY, TRUE));
|
||||||
|
g_assert_false(person_ocr_projection_service_apply(f.d, PERSON, a, &e));
|
||||||
|
g_assert_nonnull(e);
|
||||||
|
g_clear_error(&e);
|
||||||
|
PersonOcrProjectionDao *dao = person_ocr_projection_dao_new(f.d);
|
||||||
|
char *value = person_ocr_projection_dao_get_value(
|
||||||
|
dao, PERSON, "given_names", &e);
|
||||||
|
g_assert_null(value);
|
||||||
|
GPtrArray *history = person_ocr_projection_dao_list(dao, PERSON, &e);
|
||||||
|
g_assert_cmpuint(history->len, ==, 0);
|
||||||
|
g_ptr_array_unref(history);
|
||||||
|
person_ocr_projection_dao_free(dao);
|
||||||
|
g_ptr_array_unref(a);
|
||||||
|
done(&f);
|
||||||
|
}
|
||||||
|
static void test_apply_stale_rollback(void) {
|
||||||
|
Fixture f = fixture();
|
||||||
|
GError *e = NULL;
|
||||||
|
GPtrArray *candidates =
|
||||||
|
person_ocr_projection_service_candidates(f.d, EVIDENCE, RUN, &e);
|
||||||
|
g_assert_no_error(e);
|
||||||
|
g_assert_cmpuint(candidates->len, ==, 2);
|
||||||
|
g_ptr_array_unref(candidates);
|
||||||
|
GPtrArray *a = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)person_ocr_field_projection_free);
|
||||||
|
g_ptr_array_add(a, projection(FIELD, "surname", "NOM SPECIMEN", "complete",
|
||||||
|
"surname", NULL, PERSON_OCR_FILL_EMPTY));
|
||||||
|
g_ptr_array_add(a, projection(FIELD2, "given_names", "PRÉNOM SPECIMEN",
|
||||||
|
"partial", "given_names", NULL,
|
||||||
|
PERSON_OCR_FILL_EMPTY));
|
||||||
|
g_assert_true(person_ocr_projection_service_apply(f.d, PERSON, a, &e));
|
||||||
|
PersonOcrProjectionDao *dao = person_ocr_projection_dao_new(f.d);
|
||||||
|
char *v = person_ocr_projection_dao_get_value(dao, PERSON, "surname", &e);
|
||||||
|
g_assert_cmpstr(v, ==, "NOM SPECIMEN");
|
||||||
|
g_free(v);
|
||||||
|
GPtrArray *h = person_ocr_projection_dao_list(dao, PERSON, &e);
|
||||||
|
g_assert_cmpuint(h->len, ==, 2);
|
||||||
|
g_ptr_array_unref(h);
|
||||||
|
sqlite3 *s = NULL;
|
||||||
|
database_close(f.d);
|
||||||
|
f.d = NULL;
|
||||||
|
sqlite3_open(f.path, &s);
|
||||||
|
exec_ok(s, "UPDATE identity_field_observations SET "
|
||||||
|
"review_status='rejected',confirmed_value=NULL,confirmation_state="
|
||||||
|
"'unconfirmed' WHERE id='" FIELD "';");
|
||||||
|
sqlite3_close(s);
|
||||||
|
f.d = database_open(f.path);
|
||||||
|
g_ptr_array_set_size(a, 0);
|
||||||
|
g_ptr_array_add(a, projection(FIELD, "surname", "NOM SPECIMEN", "complete",
|
||||||
|
"surname", "NOM SPECIMEN",
|
||||||
|
PERSON_OCR_REPLACE_EXISTING));
|
||||||
|
g_assert_false(person_ocr_projection_service_apply(f.d, PERSON, a, &e));
|
||||||
|
g_assert_nonnull(e);
|
||||||
|
g_clear_error(&e);
|
||||||
|
person_ocr_projection_dao_free(dao);
|
||||||
|
dao = person_ocr_projection_dao_new(f.d);
|
||||||
|
h = person_ocr_projection_dao_list(dao, PERSON, &e);
|
||||||
|
g_assert_cmpuint(h->len, ==, 2);
|
||||||
|
g_ptr_array_unref(h);
|
||||||
|
person_ocr_projection_dao_free(dao);
|
||||||
|
g_ptr_array_unref(a);
|
||||||
|
done(&f);
|
||||||
|
}
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
g_test_init(&argc, &argv, NULL);
|
||||||
|
g_test_add_func("/projection/model-mapping", test_model_mapping);
|
||||||
|
g_test_add_func("/projection/apply-stale-rollback",
|
||||||
|
test_apply_stale_rollback);
|
||||||
|
g_test_add_func("/projection/conflicts-foreign-sources",
|
||||||
|
test_conflicts_and_foreign_sources);
|
||||||
|
g_test_add_func("/projection/mid-series-rollback", test_mid_series_rollback);
|
||||||
|
return g_test_run();
|
||||||
|
}
|
||||||
115
tests/test_person_ocr_projection_editor_gtk.c
Normal file
115
tests/test_person_ocr_projection_editor_gtk.c
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
#include "views/person_ocr_projection_editor.h"
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
#define EVIDENCE "10000000-0000-4000-8000-000000000019"
|
||||||
|
static GtkWidget *named(GtkWidget *w, const char *n) {
|
||||||
|
if (g_strcmp0(gtk_widget_get_name(w), n) == 0)
|
||||||
|
return w;
|
||||||
|
for (GtkWidget *c = gtk_widget_get_first_child(w); c;
|
||||||
|
c = gtk_widget_get_next_sibling(c)) {
|
||||||
|
GtkWidget *f = named(c, n);
|
||||||
|
if (f)
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
static guint count_named(GtkWidget *w, const char *name) {
|
||||||
|
guint count = g_strcmp0(gtk_widget_get_name(w), name) == 0 ? 1 : 0;
|
||||||
|
for (GtkWidget *child = gtk_widget_get_first_child(w); child;
|
||||||
|
child = gtk_widget_get_next_sibling(child))
|
||||||
|
count += count_named(child, name);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
static GtkWidget *label(GtkWidget *w, const char *text) {
|
||||||
|
if (GTK_IS_LABEL(w) && g_strcmp0(gtk_label_get_text(GTK_LABEL(w)), text) == 0)
|
||||||
|
return w;
|
||||||
|
for (GtkWidget *c = gtk_widget_get_first_child(w); c;
|
||||||
|
c = gtk_widget_get_next_sibling(c)) {
|
||||||
|
GtkWidget *found = label(c, text);
|
||||||
|
if (found)
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
static void test_editor(void) {
|
||||||
|
PersonOcrProjectionEditor *e = person_ocr_projection_editor_new();
|
||||||
|
GtkWidget *root = person_ocr_projection_editor_get_widget(e);
|
||||||
|
g_object_ref_sink(root);
|
||||||
|
person_ocr_projection_editor_set_runs(e, NULL);
|
||||||
|
GtkWidget *empty = label(root,
|
||||||
|
"Aucune donnée OCR confirmée ne peut être appliquée à cette personne.\n"
|
||||||
|
"La transcription OCR reste enregistrée dans la preuve.");
|
||||||
|
GtkWidget *title = label(root,
|
||||||
|
"Appliquer des données OCR à la personne — facultatif");
|
||||||
|
g_assert_nonnull(empty);
|
||||||
|
g_assert_nonnull(title);
|
||||||
|
g_assert_true(gtk_widget_get_visible(empty));
|
||||||
|
g_assert_false(gtk_widget_get_visible(title));
|
||||||
|
GPtrArray *out = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
g_assert_true(person_ocr_projection_editor_collect(e, &out, &error));
|
||||||
|
g_assert_cmpuint(out->len, ==, 0);
|
||||||
|
g_ptr_array_unref(out);
|
||||||
|
IdentityOcrRun *r = identity_ocr_run_new(
|
||||||
|
EVIDENCE,
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"identity_card", "front", 1, "fra", "none");
|
||||||
|
IdentityFieldObservation *f =
|
||||||
|
identity_field_observation_new("surname", "BRUT SPECIMEN", 90, NULL, 0);
|
||||||
|
identity_field_observation_accept(f);
|
||||||
|
identity_field_observation_set_normalized_value(f, "NORMALISÉ SPECIMEN");
|
||||||
|
identity_field_observation_confirm(f, "NOM SPECIMEN");
|
||||||
|
identity_ocr_run_add_field(r, f);
|
||||||
|
IdentityFieldObservation *partial = identity_field_observation_new(
|
||||||
|
"given_names", "BRUT PARTIEL", 72, NULL, 1);
|
||||||
|
identity_field_observation_modify(partial, "CORRIGÉ PARTIEL", NULL);
|
||||||
|
identity_field_observation_set_value_quality(partial, "partial");
|
||||||
|
identity_field_observation_confirm(partial, "PRÉNOM SPECIMEN");
|
||||||
|
identity_ocr_run_add_field(r, partial);
|
||||||
|
IdentityFieldObservation *rejected = identity_field_observation_new(
|
||||||
|
"nationality", "REJETÉ SPECIMEN", 40, NULL, 2);
|
||||||
|
identity_field_observation_reject(rejected);
|
||||||
|
identity_ocr_run_add_field(r, rejected);
|
||||||
|
IdentityFieldObservation *unmapped = identity_field_observation_new(
|
||||||
|
"document_number", "DOCUMENT SPECIMEN", 95, NULL, 3);
|
||||||
|
identity_field_observation_accept(unmapped);
|
||||||
|
identity_field_observation_confirm(unmapped, "DOCUMENT SPECIMEN");
|
||||||
|
identity_ocr_run_add_field(r, unmapped);
|
||||||
|
GPtrArray *runs = g_ptr_array_new();
|
||||||
|
g_ptr_array_add(runs, r);
|
||||||
|
person_ocr_projection_editor_set_runs(e, runs);
|
||||||
|
g_assert_false(gtk_widget_get_visible(empty));
|
||||||
|
g_assert_true(gtk_widget_get_visible(title));
|
||||||
|
g_assert_cmpuint(count_named(root, "projection-select"), ==, 2);
|
||||||
|
GtkCheckButton *select = GTK_CHECK_BUTTON(named(root, "projection-select"));
|
||||||
|
g_assert_false(gtk_check_button_get_active(select));
|
||||||
|
gtk_check_button_set_active(select, TRUE);
|
||||||
|
g_assert_false(person_ocr_projection_editor_collect(e, &out, &error));
|
||||||
|
g_assert_nonnull(error);
|
||||||
|
g_assert_true(gtk_check_button_get_active(select));
|
||||||
|
g_clear_error(&error);
|
||||||
|
GtkDropDown *target = GTK_DROP_DOWN(named(root, "projection-target"));
|
||||||
|
GtkDropDown *strategy = GTK_DROP_DOWN(named(root, "projection-strategy"));
|
||||||
|
gtk_drop_down_set_selected(target, 1);
|
||||||
|
gtk_drop_down_set_selected(strategy, 1);
|
||||||
|
g_assert_true(person_ocr_projection_editor_collect(e, &out, &error));
|
||||||
|
g_assert_cmpuint(out->len, ==, 1);
|
||||||
|
g_ptr_array_unref(out);
|
||||||
|
GString *summary = g_string_new(NULL);
|
||||||
|
person_ocr_projection_editor_append_summary(e, summary);
|
||||||
|
g_assert_nonnull(strstr(summary->str,
|
||||||
|
"Valeurs OCR choisies pour projection"));
|
||||||
|
g_assert_nonnull(strstr(summary->str, "surname : NOM SPECIMEN"));
|
||||||
|
g_string_free(summary, TRUE);
|
||||||
|
person_ocr_projection_editor_set_runs(e, NULL);
|
||||||
|
g_assert_cmpuint(count_named(root, "projection-select"), ==, 0);
|
||||||
|
g_ptr_array_unref(runs);
|
||||||
|
identity_ocr_run_free(r);
|
||||||
|
g_object_unref(root);
|
||||||
|
person_ocr_projection_editor_free(e);
|
||||||
|
}
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
gtk_init();
|
||||||
|
g_test_init(&argc, &argv, NULL);
|
||||||
|
g_test_add_func("/projection/editor", test_editor);
|
||||||
|
return g_test_run();
|
||||||
|
}
|
||||||
473
ticket109.md
Normal file
473
ticket109.md
Normal file
|
|
@ -0,0 +1,473 @@
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Lors de la création d’une personne dans une enquête, l’utilisateur doit
|
||||||
|
actuellement renseigner les données manuellement et ne peut pas choisir
|
||||||
|
clairement la nature de la personne ni associer facilement une preuve.
|
||||||
|
|
||||||
|
Le parcours devient particulièrement long lorsque les noms de fichiers
|
||||||
|
ne permettent pas de savoir ce qu’ils contiennent.
|
||||||
|
|
||||||
|
Le cas des documents d’identité nécessite également un traitement
|
||||||
|
forensique strict :
|
||||||
|
|
||||||
|
- conservation de l’original ;
|
||||||
|
- analyse sur une copie de travail ;
|
||||||
|
- OCR contrôlé ;
|
||||||
|
- validation humaine ;
|
||||||
|
- distinction entre texte brut, valeur normalisée et correction manuelle ;
|
||||||
|
- aucune affirmation automatique concernant l’identité réelle ou
|
||||||
|
l’authenticité du document.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Créer un assistant fluide permettant, depuis la fenêtre de création d’une
|
||||||
|
personne :
|
||||||
|
|
||||||
|
1. de sélectionner son rôle ou sa nature dans l’enquête ;
|
||||||
|
2. de choisir une preuve déjà importée ;
|
||||||
|
3. d’importer immédiatement une nouvelle preuve ;
|
||||||
|
4. de visualiser la preuve avant de la sélectionner ;
|
||||||
|
5. de qualifier le type de preuve ;
|
||||||
|
6. de lancer une analyse adaptée au type choisi ;
|
||||||
|
7. d’utiliser l’OCR pour préremplir les champs d’un document d’identité ;
|
||||||
|
8. de corriger manuellement les données proposées ;
|
||||||
|
9. de conserver la provenance complète de chaque valeur ;
|
||||||
|
10. de garantir l’intégrité du fichier original.
|
||||||
|
|
||||||
|
## Terminologie
|
||||||
|
|
||||||
|
Ne pas confondre :
|
||||||
|
|
||||||
|
- le rôle de la personne dans l’enquête ;
|
||||||
|
- son niveau d’identification ;
|
||||||
|
- l’authenticité du document présenté ;
|
||||||
|
- la confiance accordée aux différentes observations.
|
||||||
|
|
||||||
|
Une personne peut par exemple avoir :
|
||||||
|
|
||||||
|
- rôle : Identité présentée ;
|
||||||
|
- identification : Non vérifiée ;
|
||||||
|
- document : Authenticité indéterminée ;
|
||||||
|
- hypothèse : Identité potentiellement usurpée.
|
||||||
|
|
||||||
|
L’application ne doit jamais transformer automatiquement cette personne
|
||||||
|
en auteur identifié.
|
||||||
|
|
||||||
|
## Fenêtre de création d’une personne
|
||||||
|
|
||||||
|
Ajouter un champ contrôlé :
|
||||||
|
|
||||||
|
Rôle dans l’enquête
|
||||||
|
|
||||||
|
Valeurs initiales proposées :
|
||||||
|
|
||||||
|
- Auteur présumé ;
|
||||||
|
- Identité présentée ;
|
||||||
|
- Identité potentiellement usurpée ;
|
||||||
|
- Victime ;
|
||||||
|
- Témoin ;
|
||||||
|
- Titulaire bancaire déclaré ;
|
||||||
|
- Intermédiaire ;
|
||||||
|
- Personne citée ;
|
||||||
|
- Autre.
|
||||||
|
|
||||||
|
Ces valeurs doivent provenir du vocabulaire contrôlé du projet.
|
||||||
|
|
||||||
|
Le champ existant « Identification » reste distinct et conserve des états
|
||||||
|
tels que :
|
||||||
|
|
||||||
|
- Inconnu ;
|
||||||
|
- Non vérifié ;
|
||||||
|
- Partiellement identifié ;
|
||||||
|
- Identifié ;
|
||||||
|
- Contesté.
|
||||||
|
|
||||||
|
## Association d’une preuve
|
||||||
|
|
||||||
|
Depuis la fenêtre de création d’une personne, proposer deux actions :
|
||||||
|
|
||||||
|
- Sélectionner une preuve existante ;
|
||||||
|
- Importer une nouvelle preuve.
|
||||||
|
|
||||||
|
L’utilisateur ne doit pas être obligé de fermer la fenêtre, importer la
|
||||||
|
preuve ailleurs, puis recommencer la création de la personne.
|
||||||
|
|
||||||
|
### Preuve existante
|
||||||
|
|
||||||
|
La sélection doit afficher au minimum :
|
||||||
|
|
||||||
|
- miniature ou aperçu ;
|
||||||
|
- nom du fichier ;
|
||||||
|
- type de preuve ;
|
||||||
|
- taille ;
|
||||||
|
- date d’import ;
|
||||||
|
- empreinte SHA-256 abrégée ;
|
||||||
|
- description éventuelle ;
|
||||||
|
- état d’intégrité.
|
||||||
|
|
||||||
|
Prévoir une recherche et un filtrage par type.
|
||||||
|
|
||||||
|
### Nouvelle preuve
|
||||||
|
|
||||||
|
L’import déclenché depuis la fenêtre doit utiliser le mécanisme forensique
|
||||||
|
central du projet :
|
||||||
|
|
||||||
|
- copie contrôlée dans l’enquête ;
|
||||||
|
- calcul du SHA-256 ;
|
||||||
|
- conservation du nom original ;
|
||||||
|
- enregistrement de la provenance ;
|
||||||
|
- aucune modification du fichier source ;
|
||||||
|
- détection du type MIME réel ;
|
||||||
|
- détection des collisions ;
|
||||||
|
- traitement asynchrone ;
|
||||||
|
- annulation sûre.
|
||||||
|
|
||||||
|
À la fin de l’import, la nouvelle preuve doit être automatiquement
|
||||||
|
sélectionnée dans la fenêtre de création de la personne.
|
||||||
|
|
||||||
|
## Aperçu des preuves
|
||||||
|
|
||||||
|
La fenêtre de sélection et d’import doit fournir un aperçu suffisamment
|
||||||
|
grand pour identifier le contenu sans occuper toute la fenêtre.
|
||||||
|
|
||||||
|
Prévoir une zone responsive avec :
|
||||||
|
|
||||||
|
- conservation du ratio ;
|
||||||
|
- ajustement à la zone ;
|
||||||
|
- zoom avant et arrière ;
|
||||||
|
- retour à l’ajustement ;
|
||||||
|
- défilement lorsque l’image est agrandie ;
|
||||||
|
- message explicite lorsque l’aperçu est indisponible.
|
||||||
|
|
||||||
|
Prise en charge minimale :
|
||||||
|
|
||||||
|
- image : aperçu direct ;
|
||||||
|
- PDF : première page avec navigation entre les pages ;
|
||||||
|
- vidéo : miniature, durée et informations principales ;
|
||||||
|
- EML : résumé des en-têtes et liste des pièces jointes ;
|
||||||
|
- autre fichier : icône, type MIME, taille et métadonnées disponibles.
|
||||||
|
|
||||||
|
L’aperçu est une représentation dérivée.
|
||||||
|
|
||||||
|
Il ne doit jamais modifier le fichier original.
|
||||||
|
|
||||||
|
Toute conversion, génération de miniature, rotation, amélioration,
|
||||||
|
redimensionnement ou rendu PDF doit être réalisée dans une zone de
|
||||||
|
travail temporaire contrôlée.
|
||||||
|
|
||||||
|
## Qualification du type de preuve
|
||||||
|
|
||||||
|
Permettre de sélectionner ou confirmer un type contrôlé, par exemple :
|
||||||
|
|
||||||
|
- Document d’identité ;
|
||||||
|
- Carte nationale d’identité ;
|
||||||
|
- Passeport ;
|
||||||
|
- Permis de conduire ;
|
||||||
|
- Document bancaire ;
|
||||||
|
- Capture d’écran ;
|
||||||
|
- Conversation ;
|
||||||
|
- Courrier électronique ;
|
||||||
|
- PDF ;
|
||||||
|
- Photo ;
|
||||||
|
- Vidéo ;
|
||||||
|
- Billet ou justificatif de commande ;
|
||||||
|
- Autre.
|
||||||
|
|
||||||
|
La détection automatique peut proposer un type, mais l’utilisateur doit
|
||||||
|
toujours pouvoir le corriger avant validation.
|
||||||
|
|
||||||
|
Le type choisi détermine les analyses proposées.
|
||||||
|
|
||||||
|
## Document d’identité
|
||||||
|
|
||||||
|
Lorsqu’une preuve est qualifiée comme document d’identité, proposer :
|
||||||
|
|
||||||
|
Analyser le document
|
||||||
|
|
||||||
|
Le traitement doit être asynchrone et annulable.
|
||||||
|
|
||||||
|
Il doit :
|
||||||
|
|
||||||
|
1. vérifier l’intégrité de la preuve ;
|
||||||
|
2. créer une copie de travail ;
|
||||||
|
3. conserver l’original strictement intact ;
|
||||||
|
4. appliquer les conversions uniquement à la copie ;
|
||||||
|
5. exécuter l’OCR ;
|
||||||
|
6. conserver le texte OCR brut ;
|
||||||
|
7. proposer des champs structurés ;
|
||||||
|
8. attendre une validation humaine avant toute intégration.
|
||||||
|
|
||||||
|
## Champs OCR proposés
|
||||||
|
|
||||||
|
Selon les informations réellement visibles, proposer notamment :
|
||||||
|
|
||||||
|
- nom ;
|
||||||
|
- prénoms ;
|
||||||
|
- sexe déclaré ;
|
||||||
|
- date de naissance ;
|
||||||
|
- lieu de naissance ;
|
||||||
|
- nationalité déclarée ;
|
||||||
|
- taille ;
|
||||||
|
- numéro du document ;
|
||||||
|
- date de délivrance ;
|
||||||
|
- date d’expiration ;
|
||||||
|
- autorité de délivrance ;
|
||||||
|
- zone lisible par machine ;
|
||||||
|
- autres valeurs observées.
|
||||||
|
|
||||||
|
Ne jamais inventer une valeur absente ou illisible.
|
||||||
|
|
||||||
|
Chaque proposition doit posséder un état :
|
||||||
|
|
||||||
|
- détectée ;
|
||||||
|
- partielle ;
|
||||||
|
- incertaine ;
|
||||||
|
- invalide ;
|
||||||
|
- corrigée ;
|
||||||
|
- rejetée ;
|
||||||
|
- confirmée.
|
||||||
|
|
||||||
|
## Validation et correction humaine
|
||||||
|
|
||||||
|
Avant la création de la personne, afficher côte à côte autant que possible :
|
||||||
|
|
||||||
|
- l’aperçu du document ;
|
||||||
|
- les valeurs extraites ;
|
||||||
|
- les champs modifiables ;
|
||||||
|
- le niveau de confiance OCR ;
|
||||||
|
- la provenance précise.
|
||||||
|
|
||||||
|
Pour chaque champ, conserver séparément :
|
||||||
|
|
||||||
|
- valeur OCR brute ;
|
||||||
|
- valeur normalisée ;
|
||||||
|
- valeur corrigée manuellement ;
|
||||||
|
- valeur finalement confirmée ;
|
||||||
|
- méthode d’extraction ;
|
||||||
|
- langue OCR ;
|
||||||
|
- version de l’outil ;
|
||||||
|
- date UTC ;
|
||||||
|
- preuve source ;
|
||||||
|
- page ou zone source lorsque disponible.
|
||||||
|
|
||||||
|
Une correction manuelle ne doit jamais écraser le résultat OCR brut.
|
||||||
|
|
||||||
|
L’utilisateur doit pouvoir :
|
||||||
|
|
||||||
|
- modifier une proposition ;
|
||||||
|
- rejeter une proposition ;
|
||||||
|
- laisser un champ vide ;
|
||||||
|
- revenir à la valeur OCR ;
|
||||||
|
- confirmer uniquement certains champs.
|
||||||
|
|
||||||
|
## Authenticité et identité usurpée
|
||||||
|
|
||||||
|
Ajouter un statut contrôlé pour le document :
|
||||||
|
|
||||||
|
- Authenticité indéterminée ;
|
||||||
|
- Présumé authentique ;
|
||||||
|
- Suspect ;
|
||||||
|
- Présumé falsifié ;
|
||||||
|
- Falsifié confirmé ;
|
||||||
|
- Document usurpé présumé ;
|
||||||
|
- Document usurpé confirmé.
|
||||||
|
|
||||||
|
Les statuts affirmatifs doivent nécessiter une validation explicite et
|
||||||
|
une justification.
|
||||||
|
|
||||||
|
L’OCR ne doit jamais conclure :
|
||||||
|
|
||||||
|
- que le document est authentique ;
|
||||||
|
- que la personne figurant sur le document est l’auteur ;
|
||||||
|
- que l’identité est réellement usurpée ;
|
||||||
|
- que le titulaire a participé aux faits.
|
||||||
|
|
||||||
|
Dans le cas courant, le document doit pouvoir être enregistré comme :
|
||||||
|
|
||||||
|
Identité présentée — authenticité indéterminée
|
||||||
|
|
||||||
|
avec une hypothèse séparée :
|
||||||
|
|
||||||
|
Identité potentiellement usurpée
|
||||||
|
|
||||||
|
## Création de la personne
|
||||||
|
|
||||||
|
Après validation, créer la personne avec uniquement les champs confirmés.
|
||||||
|
|
||||||
|
La personne doit être liée à la preuve par une relation factuelle, par
|
||||||
|
exemple :
|
||||||
|
|
||||||
|
- Identité observée dans ;
|
||||||
|
- Document présenté au nom de ;
|
||||||
|
- Données extraites depuis ;
|
||||||
|
- Identité déclarée dans.
|
||||||
|
|
||||||
|
Ne pas créer automatiquement une relation :
|
||||||
|
|
||||||
|
- Est l’auteur ;
|
||||||
|
- Identité réelle de ;
|
||||||
|
- A usurpé l’identité de.
|
||||||
|
|
||||||
|
Les résultats OCR rejetés ne doivent pas devenir des attributs de la
|
||||||
|
personne.
|
||||||
|
|
||||||
|
## Intégrité et provenance
|
||||||
|
|
||||||
|
Le fichier original doit rester immuable.
|
||||||
|
|
||||||
|
Avant chaque analyse :
|
||||||
|
|
||||||
|
- recalculer ou vérifier le SHA-256 ;
|
||||||
|
- bloquer l’analyse en cas de divergence ;
|
||||||
|
- ne jamais réécrire l’empreinte enregistrée pour masquer une différence.
|
||||||
|
|
||||||
|
Tous les fichiers dérivés doivent être identifiables comme tels :
|
||||||
|
|
||||||
|
- miniature ;
|
||||||
|
- rendu de page PDF ;
|
||||||
|
- image préparée pour OCR ;
|
||||||
|
- recadrage ;
|
||||||
|
- correction d’orientation ;
|
||||||
|
- texte OCR ;
|
||||||
|
- JSON de métadonnées.
|
||||||
|
|
||||||
|
Chaque dérivé doit conserver :
|
||||||
|
|
||||||
|
- preuve parente ;
|
||||||
|
- outil ;
|
||||||
|
- version ;
|
||||||
|
- arguments ;
|
||||||
|
- date UTC ;
|
||||||
|
- SHA-256 ;
|
||||||
|
- statut ;
|
||||||
|
- avertissements.
|
||||||
|
|
||||||
|
## Interface
|
||||||
|
|
||||||
|
Le parcours doit rester possible depuis une seule fenêtre ou un assistant
|
||||||
|
cohérent :
|
||||||
|
|
||||||
|
Création de la personne
|
||||||
|
↓
|
||||||
|
choix du rôle
|
||||||
|
↓
|
||||||
|
sélection ou import de la preuve
|
||||||
|
↓
|
||||||
|
aperçu
|
||||||
|
↓
|
||||||
|
qualification du type
|
||||||
|
↓
|
||||||
|
analyse facultative
|
||||||
|
↓
|
||||||
|
révision des propositions
|
||||||
|
↓
|
||||||
|
création de la personne et des relations confirmées
|
||||||
|
|
||||||
|
Utiliser des boutons compacts avec icônes et infobulles.
|
||||||
|
|
||||||
|
Ne pas ajouter de gros boutons occupant inutilement l’interface.
|
||||||
|
|
||||||
|
La fermeture ou l’annulation ne doit créer ni personne partielle, ni
|
||||||
|
attribut partiel, ni relation partielle.
|
||||||
|
|
||||||
|
## Transactions
|
||||||
|
|
||||||
|
La création finale doit être transactionnelle pour :
|
||||||
|
|
||||||
|
- la personne ;
|
||||||
|
- ses attributs confirmés ;
|
||||||
|
- le rattachement à la preuve ;
|
||||||
|
- les observations OCR confirmées ;
|
||||||
|
- les relations ;
|
||||||
|
- les références aux fichiers dérivés conservés.
|
||||||
|
|
||||||
|
En cas d’échec :
|
||||||
|
|
||||||
|
- rollback complet ;
|
||||||
|
- original inchangé ;
|
||||||
|
- aucune personne incomplète ;
|
||||||
|
- aucune relation orpheline ;
|
||||||
|
- aucun fichier temporaire présenté comme preuve permanente.
|
||||||
|
|
||||||
|
## Tests obligatoires
|
||||||
|
|
||||||
|
Ajouter des fixtures exclusivement synthétiques.
|
||||||
|
|
||||||
|
Couvrir au minimum :
|
||||||
|
|
||||||
|
1. choix de chaque rôle contrôlé ;
|
||||||
|
2. sélection d’une preuve existante ;
|
||||||
|
3. import d’une nouvelle preuve depuis la fenêtre ;
|
||||||
|
4. sélection automatique après import ;
|
||||||
|
5. aperçu image ;
|
||||||
|
6. aperçu PDF ;
|
||||||
|
7. aperçu indisponible ;
|
||||||
|
8. navigation entre pages ;
|
||||||
|
9. qualification manuelle du type ;
|
||||||
|
10. document d’identité déclenchant l’OCR ;
|
||||||
|
11. autre type ne déclenchant pas automatiquement l’OCR ;
|
||||||
|
12. original inchangé ;
|
||||||
|
13. SHA-256 inchangé ;
|
||||||
|
14. travail sur copie ;
|
||||||
|
15. texte OCR brut conservé ;
|
||||||
|
16. valeur normalisée séparée ;
|
||||||
|
17. correction manuelle séparée ;
|
||||||
|
18. rejet d’un champ ;
|
||||||
|
19. champ illisible laissé vide ;
|
||||||
|
20. création avec seulement les valeurs confirmées ;
|
||||||
|
21. provenance par champ ;
|
||||||
|
22. annulation ;
|
||||||
|
23. rollback ;
|
||||||
|
24. fermeture de fenêtre pendant l’analyse ;
|
||||||
|
25. changement d’enquête pendant l’analyse ;
|
||||||
|
26. aucune écriture dans la mauvaise base ;
|
||||||
|
27. aucune donnée réelle dans les fixtures ;
|
||||||
|
28. aucune conclusion automatique sur l’authenticité ;
|
||||||
|
29. aucune fusion automatique avec l’auteur présumé ;
|
||||||
|
30. réouverture de l’enquête avec données persistantes.
|
||||||
|
|
||||||
|
Séparer autant que possible :
|
||||||
|
|
||||||
|
- services d’import ;
|
||||||
|
- génération d’aperçu ;
|
||||||
|
- OCR ;
|
||||||
|
- modèle de révision ;
|
||||||
|
- DAO ;
|
||||||
|
- tests GTK ciblés ;
|
||||||
|
- validation visuelle manuelle.
|
||||||
|
|
||||||
|
## Critères d’acceptation
|
||||||
|
|
||||||
|
Le ticket est terminé seulement si :
|
||||||
|
|
||||||
|
- le rôle de la personne peut être sélectionné ;
|
||||||
|
- une preuve existante peut être choisie ;
|
||||||
|
- une nouvelle preuve peut être importée sans quitter le parcours ;
|
||||||
|
- un aperçu exploitable est disponible ;
|
||||||
|
- le type de preuve peut être confirmé ou corrigé ;
|
||||||
|
- un document d’identité peut être analysé par OCR ;
|
||||||
|
- les propositions OCR sont modifiables avant intégration ;
|
||||||
|
- le texte OCR brut n’est jamais écrasé ;
|
||||||
|
- l’original reste inchangé ;
|
||||||
|
- la provenance est persistée ;
|
||||||
|
- seules les valeurs confirmées alimentent la personne ;
|
||||||
|
- aucune identité n’est attribuée automatiquement à l’auteur présumé ;
|
||||||
|
- tous les tests passent.
|
||||||
|
|
||||||
|
## Sécurité de développement
|
||||||
|
|
||||||
|
Codex et tout agent de développement doivent travailler uniquement avec :
|
||||||
|
|
||||||
|
- le dépôt source ;
|
||||||
|
- des fixtures synthétiques ;
|
||||||
|
- des bases SQLite temporaires synthétiques.
|
||||||
|
|
||||||
|
Ils ne doivent jamais accéder à :
|
||||||
|
|
||||||
|
- Enquete.sqlite réelle ;
|
||||||
|
- documents d’identité réels ;
|
||||||
|
- captures réelles ;
|
||||||
|
- e-mails réels ;
|
||||||
|
- données bancaires réelles ;
|
||||||
|
- vidéos réelles ;
|
||||||
|
- autres preuves de l’enquête.
|
||||||
Loading…
Reference in a new issue