feat(person): projeter explicitement les valeurs OCR confirmées

This commit is contained in:
grayTerminal-sh 2026-08-02 08:33:30 +02:00
parent b8debcde2d
commit 78f0e45450
50 changed files with 2025 additions and 116 deletions

View file

@ -2,6 +2,8 @@
### Added ### Added
- Projection contrôlée des champs OCR confirmés vers les attributs structurés
dune personne, avec provenance append-only, conflits explicites et rollback.
- Saisie et consultation, depuis la fiche preuve, dun historique immuable - Saisie et consultation, depuis la fiche preuve, dun historique immuable
dappréciations humaines dauthenticité documentaire avec justification et dappréciations humaines dauthenticité documentaire avec justification et
`OcrRun` facultatif appartenant à la preuve, sans verdict automatique. `OcrRun` facultatif appartenant à la preuve, sans verdict automatique.

View file

@ -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)

View file

@ -20,6 +20,10 @@ Le projet vise à fournir un environnement local, modulaire et traçable pour or
La fiche dune preuve permet aussi de consigner une appréciation humaine La fiche dune preuve permet aussi de consigner une appréciation humaine
dauthenticité documentaire. Cet historique est immuable, justifié selon le dauthenticité documentaire. Cet historique est immuable, justifié selon le
statut choisi et ne peut jamais être produit automatiquement par lOCR. statut choisi et ne peut jamais être produit automatiquement par lOCR.
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
View 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;

View file

@ -128,6 +128,12 @@ lauthenticité à `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 à lappréciation courante. chaîne son identifiant à lappréciation courante.
La V19 ajoute les champs de profil structurés et lhistorique 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

View file

@ -20,6 +20,11 @@ Lauthenticité 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 laperçu partagé comprend La validation ciblée de laperç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

View file

@ -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 lauthenticité documentaire - interface GTK de saisie et de consultation de lauthenticité 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 didentification et des rôles V18 2. les interfaces GTK complètes des états didentification 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é.

View file

@ -21,8 +21,13 @@ appartient à la même preuve. `previous_assessment_id` forme une chaîne
append-only ; lajout et la détermination du maillon courant sont coordonnés append-only ; lajout 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`

View file

@ -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;

View file

@ -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,

View file

@ -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

View 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

View 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

View 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

View file

@ -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.

View file

@ -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(

View 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

View file

@ -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

View file

@ -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. */

View 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

View 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

View file

@ -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.

View file

@ -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.

View file

@ -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. */

View file

@ -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",

View file

@ -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 douvrir la base de lenquête."); "Impossible douvrir la base de lenquê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,

View file

@ -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;
}

View 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;
}

View 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 lexé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,
"Lexé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 nest plus projetable.");
person_ocr_projection_dao_free(dao);
identity_ocr_dao_free(ocr);
return FALSE;
}

View file

@ -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 lOCR didentité.");return FALSE; "Impossible de conserver lOCR didentité.");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;
} }

View 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;
}

View file

@ -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 dannuler 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;
} }

View file

@ -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
) )

View file

@ -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;

View 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;
}

View file

@ -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 nest encore persistée."); "OCR contrôlé en cours… Aucune donnée nest 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é\nLOCR peut contenir des erreurs. Ces "
"informations sont seulement présentées sur le document ; "
"lauthenticité nest pas établie et aucune fusion de personne "
"ne sera effectuée. Aucune écriture na 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;
}

View file

@ -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(

View 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. LOCR peut contenir des "
"erreurs : lauthenticité nest 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);
}

View file

@ -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);

View 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 dapplication.");
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);
}

View file

@ -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
) )

View file

@ -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(

View file

@ -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),
"LOCR produit des propositions à vérifier")); "LOCR 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),

View file

@ -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é nest pas établie")); g_assert_nonnull(find_label(root,"authenticité nest pas établie"));

View file

@ -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 "

View file

@ -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));

View file

@ -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);

View 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();
}

View 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
View file

@ -0,0 +1,473 @@
## Contexte
Lors de la création dune personne dans une enquête, lutilisateur 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 quils contiennent.
Le cas des documents didentité nécessite également un traitement
forensique strict :
- conservation de loriginal ;
- 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 lidentité réelle ou
lauthenticité du document.
## Objectif
Créer un assistant fluide permettant, depuis la fenêtre de création dune
personne :
1. de sélectionner son rôle ou sa nature dans lenquête ;
2. de choisir une preuve déjà importée ;
3. dimporter 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. dutiliser lOCR pour préremplir les champs dun document didentité ;
8. de corriger manuellement les données proposées ;
9. de conserver la provenance complète de chaque valeur ;
10. de garantir lintégrité du fichier original.
## Terminologie
Ne pas confondre :
- le rôle de la personne dans lenquête ;
- son niveau didentification ;
- lauthenticité 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.
Lapplication ne doit jamais transformer automatiquement cette personne
en auteur identifié.
## Fenêtre de création dune personne
Ajouter un champ contrôlé :
Rôle dans lenquê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 dune preuve
Depuis la fenêtre de création dune personne, proposer deux actions :
- Sélectionner une preuve existante ;
- Importer une nouvelle preuve.
Lutilisateur 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 dimport ;
- empreinte SHA-256 abrégée ;
- description éventuelle ;
- état dintégrité.
Prévoir une recherche et un filtrage par type.
### Nouvelle preuve
Limport déclenché depuis la fenêtre doit utiliser le mécanisme forensique
central du projet :
- copie contrôlée dans lenquê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 limport, 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 dimport 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 à lajustement ;
- défilement lorsque limage est agrandie ;
- message explicite lorsque laperç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.
Laperç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 didentité ;
- Carte nationale didentité ;
- 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 lutilisateur doit
toujours pouvoir le corriger avant validation.
Le type choisi détermine les analyses proposées.
## Document didentité
Lorsquune preuve est qualifiée comme document didentité, proposer :
Analyser le document
Le traitement doit être asynchrone et annulable.
Il doit :
1. vérifier lintégrité de la preuve ;
2. créer une copie de travail ;
3. conserver loriginal strictement intact ;
4. appliquer les conversions uniquement à la copie ;
5. exécuter lOCR ;
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 dexpiration ;
- 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 :
- laperç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 dextraction ;
- langue OCR ;
- version de loutil ;
- date UTC ;
- preuve source ;
- page ou zone source lorsque disponible.
Une correction manuelle ne doit jamais écraser le résultat OCR brut.
Lutilisateur 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.
LOCR ne doit jamais conclure :
- que le document est authentique ;
- que la personne figurant sur le document est lauteur ;
- que lidentité 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 lauteur ;
- Identité réelle de ;
- A usurpé lidentité 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 lanalyse en cas de divergence ;
- ne jamais réécrire lempreinte 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 dorientation ;
- 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 linterface.
La fermeture ou lannulation 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 dune preuve existante ;
3. import dune 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 didentité déclenchant lOCR ;
11. autre type ne déclenchant pas automatiquement lOCR ;
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 dun 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 lanalyse ;
25. changement denquête pendant lanalyse ;
26. aucune écriture dans la mauvaise base ;
27. aucune donnée réelle dans les fixtures ;
28. aucune conclusion automatique sur lauthenticité ;
29. aucune fusion automatique avec lauteur présumé ;
30. réouverture de lenquête avec données persistantes.
Séparer autant que possible :
- services dimport ;
- génération daperçu ;
- OCR ;
- modèle de révision ;
- DAO ;
- tests GTK ciblés ;
- validation visuelle manuelle.
## Critères dacceptation
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 didentité peut être analysé par OCR ;
- les propositions OCR sont modifiables avant intégration ;
- le texte OCR brut nest jamais écrasé ;
- loriginal reste inchangé ;
- la provenance est persistée ;
- seules les valeurs confirmées alimentent la personne ;
- aucune identité nest attribuée automatiquement à lauteur 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 didentité réels ;
- captures réelles ;
- e-mails réels ;
- données bancaires réelles ;
- vidéos réelles ;
- autres preuves de lenquête.