File size: 78,497 Bytes
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3ef569
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3ef569
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
e3ef569
7b1c987
 
 
e3ef569
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3ef569
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
 
 
 
 
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
7b1c987
 
9f9190f
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
7b1c987
 
9f9190f
 
7b1c987
 
 
 
 
 
 
 
 
 
9f9190f
7b1c987
 
 
9f9190f
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
7b1c987
 
 
9f9190f
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
7b1c987
 
 
 
 
 
9f9190f
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
7b1c987
 
9f9190f
 
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
7b1c987
 
 
 
 
 
 
 
 
9f9190f
 
 
 
 
 
7b1c987
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
e3ef569
7b1c987
 
 
 
e3ef569
7b1c987
 
 
 
 
 
 
 
 
 
 
e3ef569
9f9190f
e3ef569
9f9190f
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
e3ef569
7b1c987
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
e3ef569
 
7b1c987
 
e3ef569
7b1c987
 
e3ef569
9f9190f
e3ef569
9f9190f
7b1c987
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
e3ef569
7b1c987
 
 
 
 
 
 
9f9190f
 
e3ef569
9f9190f
7b1c987
 
 
 
 
 
 
 
 
 
 
9f9190f
e3ef569
9f9190f
7b1c987
 
 
 
 
9f9190f
 
 
 
7b1c987
 
 
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9190f
 
 
7b1c987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
"""
GraphoLab — Generatore di presentazione PowerPoint
Genera docs/GraphoLab_Presentazione.pptx (~46 slide, italiano)

Uso:
    pip install python-pptx
    python tools/generate_presentation.py
"""

import atexit
import shutil
import tempfile
from pathlib import Path

import matplotlib
matplotlib.use("Agg")  # non-interactive backend
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image

from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.util import Inches, Pt


# ---------------------------------------------------------------------------
# Palette e costanti
# ---------------------------------------------------------------------------
NAVY   = RGBColor(0x1B, 0x3A, 0x6B)
GOLD   = RGBColor(0xC8, 0x97, 0x3A)
WHITE  = RGBColor(0xFF, 0xFF, 0xFF)
DARK   = RGBColor(0x1A, 0x1A, 0x2E)
GREY   = RGBColor(0x55, 0x55, 0x55)
LIGHT  = RGBColor(0xF4, 0xF6, 0xFA)

SLIDE_W = Inches(13.33)
SLIDE_H = Inches(7.5)

TITLE_FONT = "Calibri Light"
BODY_FONT  = "Calibri"

OUTPUT  = Path(__file__).parent.parent / "docs" / "GraphoLab_Presentazione.pptx"
SAMPLES = Path(__file__).parent.parent / "data" / "samples"

# Temporary directory for matplotlib PNGs (auto-deleted on exit)
_TMP = tempfile.mkdtemp(prefix="grapholab_pptx_")
atexit.register(shutil.rmtree, _TMP, ignore_errors=True)

# Matplotlib color tuples (0-1 scale)
_NAVY_M  = (0x1B/255, 0x3A/255, 0x6B/255)
_GOLD_M  = (0xC8/255, 0x97/255, 0x3A/255)
_GREEN_M = (0x1A/255, 0x5C/255, 0x38/255)
_RED_M   = (0x8B/255, 0x1A/255, 0x1A/255)
_PURP_M  = (0x5C/255, 0x1A/255, 0x7A/255)
_LBLU_M  = (0x4A/255, 0x7D/255, 0xB8/255)


# ---------------------------------------------------------------------------
# Matplotlib diagram generators
# ---------------------------------------------------------------------------

def _mbox(ax, cx, cy, w, h, text, fc, tc="white", fs=9, bold=True):
    """Draw a rounded box centered at (cx, cy) with text."""
    patch = mpatches.FancyBboxPatch(
        (cx - w / 2, cy - h / 2), w, h,
        boxstyle="round,pad=0.08",
        facecolor=fc, edgecolor="white", linewidth=1.5,
        transform=ax.transData, clip_on=False,
    )
    ax.add_patch(patch)
    ax.text(cx, cy, text, ha="center", va="center",
            fontsize=fs, color=tc, fontweight="bold" if bold else "normal",
            multialignment="center")


def _marrow(ax, x1, y1, x2, y2, color=_GOLD_M, lw=2.0):
    """Draw an arrow from (x1,y1) to (x2,y2)."""
    ax.annotate(
        "", xy=(x2, y2), xytext=(x1, y1),
        arrowprops=dict(arrowstyle="-|>", color=color, lw=lw,
                        mutation_scale=14),
    )


def _gen_training_cycle(path: Path):
    fig, ax = plt.subplots(figsize=(5.5, 4.8), facecolor="white")
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 10)
    ax.axis("off")

    # 4 boxes: top, right, bottom, left
    boxes = [
        (5.0, 8.0, "1. Dati\netichettati",   _NAVY_M),
        (8.5, 5.0, "2. Previsione\nmodello",  _LBLU_M),
        (5.0, 2.0, "3. Calcolo\nerrore",      _RED_M),
        (1.5, 5.0, "4. Aggiorna\nparametri",  _PURP_M),
    ]
    for cx, cy, text, fc in boxes:
        _mbox(ax, cx, cy, 2.8, 1.4, text, fc, fs=9)

    # Clockwise arrows (from edge of each box to edge of next)
    pairs = [(0, 1), (1, 2), (2, 3), (3, 0)]
    midpoints = [(b[0], b[1]) for b in boxes]
    for i, j in pairs:
        x1, y1 = midpoints[i]
        x2, y2 = midpoints[j]
        # Move arrow start/end toward the neighbor
        dx, dy = x2 - x1, y2 - y1
        dist = (dx**2 + dy**2) ** 0.5
        off = 1.5
        _marrow(ax, x1 + dx/dist*off, y1 + dy/dist*off,
                    x2 - dx/dist*off, y2 - dy/dist*off, color=_GOLD_M)

    # Center label
    ax.text(5.0, 5.0, "Training\nLoop", ha="center", va="center",
            fontsize=11, color=_NAVY_M, fontweight="bold",
            bbox=dict(boxstyle="circle,pad=0.4", facecolor=(0.95, 0.97, 1.0),
                      edgecolor=_GOLD_M, linewidth=2))

    plt.tight_layout(pad=0.2)
    fig.savefig(path, dpi=150, bbox_inches="tight", facecolor="white")
    plt.close(fig)


def _gen_neural_network(path: Path):
    fig, ax = plt.subplots(figsize=(6.5, 5.0), facecolor="white")
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 10)
    ax.axis("off")

    layers = [
        (1.5, [3.0, 5.0, 7.0], _NAVY_M,  "Strato di\ninput"),
        (5.0, [2.0, 4.0, 6.0, 8.0], _GREEN_M, "Strato\nnascosto"),
        (8.5, [3.5, 6.5],    _GOLD_M,  "Strato di\noutput"),
    ]

    # Draw connections first (behind nodes)
    for (x1, ys1, _, _l1), (x2, ys2, _, _l2) in zip(layers, layers[1:]):
        for y1 in ys1:
            for y2 in ys2:
                ax.plot([x1, x2], [y1, y2], color=(0.75, 0.75, 0.85),
                        lw=0.8, zorder=1)

    # Draw nodes
    r = 0.45
    for x, ys, color, label in layers:
        for y in ys:
            circle = plt.Circle((x, y), r, facecolor=color,
                                 edgecolor="white", linewidth=2, zorder=2)
            ax.add_patch(circle)
        # Layer label at bottom
        ax.text(x, 0.7, label, ha="center", va="center",
                fontsize=9, color=_NAVY_M, fontweight="bold",
                multialignment="center")

    # Input feature labels
    feat = ["Tratto", "Forma", "Pressione"]
    for y, name in zip(layers[0][1], feat):
        ax.text(0.1, y, name, ha="left", va="center",
                fontsize=7.5, color=_NAVY_M)

    # Output labels
    out = ["Autentica", "Falsa"]
    for y, name in zip(layers[2][1], out):
        ax.text(9.2, y, name, ha="left", va="center",
                fontsize=8, color=_NAVY_M, fontweight="bold")

    ax.set_title("Rete Neurale — Architettura a Strati", fontsize=10,
                 color=_NAVY_M, fontweight="bold", pad=4)

    plt.tight_layout(pad=0.2)
    fig.savefig(path, dpi=150, bbox_inches="tight", facecolor="white")
    plt.close(fig)


def _gen_computer_vision(path: Path):
    fig, ax = plt.subplots(figsize=(9, 3.2), facecolor="white")
    ax.set_xlim(0, 18)
    ax.set_ylim(0, 4)
    ax.axis("off")

    steps = [
        (1.5, "Immagine\nmanoscritto", _NAVY_M),
        (4.8, "Griglia\npixel",        _LBLU_M),
        (8.1, "Filtri CNN\n(feature)",  _GREEN_M),
        (11.4, "Feature\nmap",          _PURP_M),
        (14.7, "Classificazione\n(output)", _GOLD_M),
    ]

    for x, label, color in steps:
        _mbox(ax, x, 2.0, 2.6, 1.5, label, color, fs=9)

    # Arrows between boxes
    for i in range(len(steps) - 1):
        x1 = steps[i][0] + 1.3
        x2 = steps[i+1][0] - 1.3
        _marrow(ax, x1, 2.0, x2, 2.0, color=_GOLD_M)

    ax.set_title("Pipeline Computer Vision", fontsize=10,
                 color=_NAVY_M, fontweight="bold", pad=4)

    plt.tight_layout(pad=0.2)
    fig.savefig(path, dpi=150, bbox_inches="tight", facecolor="white")
    plt.close(fig)


def _gen_confidence_scores(path: Path):
    fig, axes = plt.subplots(2, 1, figsize=(6, 4.5), facecolor="white")
    fig.subplots_adjust(hspace=0.55)

    examples = [
        ("Esempio 1: Firma Autentica", [91, 9],  [_GREEN_M, _RED_M]),
        ("Esempio 2: Firma Contraffatta", [12, 88], [_GREEN_M, _RED_M]),
    ]
    labels = ["Autentica", "Falsa"]

    for ax, (title, scores, colors) in zip(axes, examples):
        bars = ax.barh(labels, scores, color=colors, edgecolor="white",
                       height=0.55)
        ax.set_xlim(0, 100)
        ax.set_xlabel("Confidenza (%)", fontsize=8, color=_NAVY_M)
        ax.set_title(title, fontsize=9, color=_NAVY_M, fontweight="bold")
        ax.tick_params(labelsize=8)
        ax.spines[["top", "right"]].set_visible(False)
        for bar, score in zip(bars, scores):
            ax.text(min(score + 2, 95), bar.get_y() + bar.get_height()/2,
                    f"{score}%", va="center", fontsize=9, fontweight="bold",
                    color=_NAVY_M)

    plt.tight_layout(pad=0.4)
    fig.savefig(path, dpi=150, bbox_inches="tight", facecolor="white")
    plt.close(fig)


def _gen_siamese_network(path: Path):
    fig, ax = plt.subplots(figsize=(6.5, 5.5), facecolor="white")
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 10)
    ax.axis("off")

    # Input boxes
    _mbox(ax, 2.0, 9.0, 2.8, 1.0, "Firma di\nriferimento", _NAVY_M, fs=8)
    _mbox(ax, 8.0, 9.0, 2.8, 1.0, "Firma in\nesame", _NAVY_M, fs=8)

    # CNN boxes (shared weights)
    _mbox(ax, 2.0, 6.8, 2.8, 1.0, "CNN SigNet", _LBLU_M, fs=8)
    _mbox(ax, 8.0, 6.8, 2.8, 1.0, "CNN SigNet\n(stessi pesi)", _LBLU_M, fs=8)

    # Embedding circles
    _mbox(ax, 2.0, 4.6, 2.8, 0.9, "Embedding A", _GREEN_M, fs=8)
    _mbox(ax, 8.0, 4.6, 2.8, 0.9, "Embedding B", _GREEN_M, fs=8)

    # Distance box
    _mbox(ax, 5.0, 2.8, 3.2, 0.9, "Distanza coseno", _GOLD_M, tc=_NAVY_M, fs=9)

    # Verdict box
    _mbox(ax, 5.0, 1.0, 4.0, 1.0,
          "AUTENTICA / FALSA\n(soglia = 0.35)", _RED_M, fs=8)

    # Arrows
    for x in [2.0, 8.0]:
        _marrow(ax, x, 8.5, x, 7.35)   # input → CNN
        _marrow(ax, x, 6.3, x, 5.05)   # CNN → embedding
        _marrow(ax, x, 4.15, 5.0, 3.25)  # embedding → distance

    _marrow(ax, 5.0, 2.35, 5.0, 1.5)   # distance → verdict

    # Shared weights label
    ax.text(5.0, 6.8, "⟵ pesi condivisi ⟶", ha="center", va="center",
            fontsize=7.5, color=_GOLD_M, fontstyle="italic")

    ax.set_title("Architettura Rete Siamese (SigNet)", fontsize=10,
                 color=_NAVY_M, fontweight="bold", pad=4)

    plt.tight_layout(pad=0.2)
    fig.savefig(path, dpi=150, bbox_inches="tight", facecolor="white")
    plt.close(fig)


def _gen_detr_detection(path: Path):
    fig, ax = plt.subplots(figsize=(5.5, 7.0), facecolor="white")
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 14)
    ax.axis("off")

    # Document background
    doc = mpatches.FancyBboxPatch((0.5, 0.5), 9.0, 13.0,
                                   boxstyle="round,pad=0.1",
                                   facecolor=(0.97, 0.97, 0.97),
                                   edgecolor=(0.6, 0.6, 0.6), linewidth=1.5)
    ax.add_patch(doc)
    ax.text(5.0, 13.2, "DOCUMENTO SCANSIONATO", ha="center", va="center",
            fontsize=8, color=(0.5, 0.5, 0.5), fontstyle="italic")

    # Horizontal lines (simulating text)
    for y in [11.5, 10.8, 10.1, 9.4, 8.7, 8.0, 7.3, 6.6]:
        w = np.random.uniform(4.0, 7.5)
        ax.plot([1.2, 1.2 + w], [y, y], color=(0.75, 0.75, 0.75), lw=4,
                solid_capstyle="round")

    # Bounding box 1 — main signature
    bb1 = mpatches.FancyBboxPatch((1.5, 1.8), 4.5, 2.8,
                                   boxstyle="square,pad=0.0",
                                   facecolor=(0.0, 0.8, 0.0, 0.08),
                                   edgecolor=_GREEN_M, linewidth=2.5)
    ax.add_patch(bb1)
    ax.text(1.7, 4.75, "Firma  94%", fontsize=8.5, color=_GREEN_M,
            fontweight="bold",
            bbox=dict(facecolor=_GREEN_M, edgecolor="none", pad=2))
    ax.text(1.6, 3.2, "~~~~~~~~~~~~~~~~", fontsize=14,
            color=(0.3, 0.3, 0.3), fontstyle="italic")

    # Bounding box 2 — second smaller signature
    bb2 = mpatches.FancyBboxPatch((5.8, 3.0), 3.0, 1.6,
                                   boxstyle="square,pad=0.0",
                                   facecolor=(0.0, 0.8, 0.0, 0.08),
                                   edgecolor=_GREEN_M, linewidth=2.5)
    ax.add_patch(bb2)
    ax.text(5.9, 4.72, "Firma  87%", fontsize=8.5, color=_GREEN_M,
            fontweight="bold",
            bbox=dict(facecolor=_GREEN_M, edgecolor="none", pad=2))
    ax.text(5.9, 3.7, "~~~~~~~~~", fontsize=14,
            color=(0.3, 0.3, 0.3), fontstyle="italic")

    ax.set_title("Conditional DETR — Rilevamento firme nel documento", fontsize=9,
                 color=_NAVY_M, fontweight="bold", pad=4)

    plt.tight_layout(pad=0.2)
    fig.savefig(path, dpi=150, bbox_inches="tight", facecolor="white")
    plt.close(fig)


def _gen_pipeline_e2e(path: Path):
    fig, ax = plt.subplots(figsize=(10.5, 4.5), facecolor="white")
    ax.set_xlim(0, 21)
    ax.set_ylim(0, 8)
    ax.axis("off")

    # Row 1: document → DETR → SigNet → Autenticità
    r1_y = 6.0
    r1 = [
        (1.5,  r1_y, "Documento\nscansionato", _NAVY_M),
        (5.0,  r1_y, "DETR\nRilevamento",    _LBLU_M),
        (8.5,  r1_y, "Firma\nestratta",         _PURP_M),
        (12.0, r1_y, "SigNet\nVerifica",        _GREEN_M),
        (16.0, r1_y, "Verdetto\nautenticità",   _GOLD_M),
    ]
    for x, y, label, fc in r1:
        _mbox(ax, x, y, 2.6, 1.2, label, fc, fs=8)
    for i in range(len(r1)-1):
        _marrow(ax, r1[i][0]+1.3, r1[i][1], r1[i+1][0]-1.3, r1[i+1][1])

    # Row 2: TrOCR → NER → Entità
    r2_y = 2.8
    r2 = [
        (5.0,  r2_y, "TrOCR\nTrascrizione", _LBLU_M),
        (8.5,  r2_y, "Testo\ntrascritto",    _NAVY_M),
        (12.0, r2_y, "WikiNEural\nNER",      _PURP_M),
        (16.0, r2_y, "Entità\n(persone, luoghi)", _GREEN_M),
    ]
    for x, y, label, fc in r2:
        _mbox(ax, x, y, 2.6, 1.2, label, fc, fs=8)
    for i in range(len(r2)-1):
        _marrow(ax, r2[i][0]+1.3, r2[i][1], r2[i+1][0]-1.3, r2[i+1][1])

    # Vertical arrow from documento (row1) down to TrOCR row
    _marrow(ax, r1[0][0], r1_y - 0.6, r2[0][0], r2_y + 0.6, color=_GOLD_M)

    # Converging arrow to final report
    _mbox(ax, 19.5, 4.4, 2.6, 1.2, "Referto\nforense", _RED_M, fs=8)
    _marrow(ax, r1[-1][0]+1.3, r1[-1][1], 19.5-1.3, 4.9, color=_GOLD_M)
    _marrow(ax, r2[-1][0]+1.3, r2[-1][1], 19.5-1.3, 3.9, color=_GOLD_M)

    ax.set_title("Pipeline End-to-End: dal documento al referto forense",
                 fontsize=10, color=_NAVY_M, fontweight="bold", pad=6)

    plt.tight_layout(pad=0.3)
    fig.savefig(path, dpi=150, bbox_inches="tight", facecolor="white")
    plt.close(fig)


def _gen_signatures_comparison(path: Path):
    """Side-by-side genuine vs forged signature images."""
    genuine_path = SAMPLES / "genuine_1_1.png"
    forged_path  = SAMPLES / "forged_1_1.png"

    if not genuine_path.exists() or not forged_path.exists():
        # Fallback: draw placeholder
        fig, axes = plt.subplots(1, 2, figsize=(7, 3), facecolor="white")
        for ax, label in zip(axes, ["Firma autentica", "Firma contraffatta"]):
            ax.text(0.5, 0.5, label, ha="center", va="center",
                    transform=ax.transAxes, fontsize=12)
            ax.axis("off")
        fig.savefig(path, dpi=150, bbox_inches="tight", facecolor="white")
        plt.close(fig)
        return

    img_g = np.array(Image.open(genuine_path).convert("RGB"))
    img_f = np.array(Image.open(forged_path).convert("RGB"))

    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7.5, 3.5),
                                    facecolor="white")

    for ax, img, label, frame_color, verdict in [
        (ax1, img_g, "Firma di riferimento", _GREEN_M, "AUTENTICA"),
        (ax2, img_f, "Firma in esame",        _RED_M,   "FALSA"),
    ]:
        ax.imshow(img, cmap="gray" if img.ndim == 2 else None)
        ax.axis("off")
        ax.set_title(label, fontsize=9, color=_NAVY_M, fontweight="bold",
                     pad=4)
        for spine in ax.spines.values():
            spine.set_visible(True)
            spine.set_edgecolor(frame_color)
            spine.set_linewidth(4)
        ax.text(0.5, -0.08, verdict, transform=ax.transAxes,
                ha="center", va="top", fontsize=10, fontweight="bold",
                color=frame_color)

    plt.suptitle("Esempio: firma autentica vs contraffatta (dataset CEDAR)",
                 fontsize=9, color=_NAVY_M, y=1.02)
    plt.tight_layout(pad=0.5)
    fig.savefig(path, dpi=150, bbox_inches="tight", facecolor="white")
    plt.close(fig)


def generate_diagrams() -> dict:
    """Generate all matplotlib PNGs and return {key: Path}."""
    tmp = Path(_TMP)
    imgs = {}

    tasks = [
        ("training_cycle",       _gen_training_cycle),
        ("neural_network",       _gen_neural_network),
        ("computer_vision",      _gen_computer_vision),
        ("confidence_scores",    _gen_confidence_scores),
        ("siamese_network",      _gen_siamese_network),
        ("detr_detection",       _gen_detr_detection),
        ("pipeline_e2e",         _gen_pipeline_e2e),
        ("signatures_comparison", _gen_signatures_comparison),
    ]

    for key, fn in tasks:
        p = tmp / f"{key}.png"
        fn(p)
        imgs[key] = p
        print(f"  [diagram] {key}.png")

    # Real sample images
    for key, filename in [
        ("handwritten", "handwritten_text_01.png"),
        ("multiline",   "handwritten_multiline_01.png"),
    ]:
        p = SAMPLES / filename
        if p.exists():
            imgs[key] = p

    return imgs


# ---------------------------------------------------------------------------
# Slide helper functions
# ---------------------------------------------------------------------------

def _blank_slide(prs: Presentation):
    return prs.slides.add_slide(prs.slide_layouts[6])


def _fill_bg(slide, color: RGBColor):
    bg = slide.background
    fill = bg.fill
    fill.solid()
    fill.fore_color.rgb = color


def _add_rect(slide, left, top, width, height, color: RGBColor):
    shape = slide.shapes.add_shape(1, left, top, width, height)
    shape.fill.solid()
    shape.fill.fore_color.rgb = color
    shape.line.fill.background()
    return shape


def _add_textbox(slide, text, left, top, width, height,
                 font_name=BODY_FONT, font_size=20,
                 color=DARK, bold=False, italic=False,
                 align=PP_ALIGN.LEFT, word_wrap=True):
    txb = slide.shapes.add_textbox(left, top, width, height)
    tf = txb.text_frame
    tf.word_wrap = word_wrap
    p = tf.paragraphs[0]
    p.alignment = align
    run = p.add_run()
    run.text = text
    run.font.name = font_name
    run.font.size = Pt(font_size)
    run.font.color.rgb = color
    run.font.bold = bold
    run.font.italic = italic
    return txb


def _add_bullet_textbox(slide, bullets, left, top, width, height,
                        font_size=18, color=DARK, indent_level=0):
    txb = slide.shapes.add_textbox(left, top, width, height)
    tf = txb.text_frame
    tf.word_wrap = True
    for i, item in enumerate(bullets):
        text, level = (item if isinstance(item, tuple) else (item, indent_level))
        p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
        p.level = level
        p.space_before = Pt(4)
        run = p.add_run()
        run.text = ("• " if level == 0 else "  – ") + text
        run.font.name = BODY_FONT
        run.font.size = Pt(font_size)
        run.font.color.rgb = color
    return txb


def _add_picture_fill(slide, img_path, left, top, max_w, max_h):
    """Add picture preserving aspect ratio within a max_w × max_h box."""
    if not img_path or not Path(img_path).exists():
        return
    img = Image.open(img_path)
    iw, ih = img.size
    scale = min(max_w / iw, max_h / ih)
    w = int(iw * scale)
    h = int(ih * scale)
    actual_left = left + (max_w - w) // 2
    actual_top  = top  + (max_h - h) // 2
    slide.shapes.add_picture(str(img_path), actual_left, actual_top, w, h)


# ---------------------------------------------------------------------------
# Slide layout builders
# ---------------------------------------------------------------------------

def _set_speaker_note(slide, text: str):
    """Write text to the PowerPoint notes pane (visible in presenter view)."""
    if not text:
        return
    tf = slide.notes_slide.notes_text_frame
    tf.text = text.strip()


def add_cover(prs: Presentation, speaker_note: str = ""):
    slide = _blank_slide(prs)
    _fill_bg(slide, NAVY)
    _add_rect(slide, 0, SLIDE_H - Inches(0.55), SLIDE_W, Inches(0.55), GOLD)
    _add_textbox(
        slide, "Intelligenza Artificiale\ne Grafologia Forense",
        Inches(1), Inches(1.2), Inches(11.3), Inches(2.8),
        font_name=TITLE_FONT, font_size=44, color=WHITE, bold=True,
        align=PP_ALIGN.CENTER,
    )
    _add_textbox(
        slide, "Nuovi strumenti a supporto dell'esaminatore forense",
        Inches(1), Inches(4.1), Inches(11.3), Inches(0.8),
        font_name=BODY_FONT, font_size=24, color=GOLD, align=PP_ALIGN.CENTER,
    )
    _add_textbox(
        slide, "GraphoLab",
        Inches(0.4), SLIDE_H - Inches(0.5), Inches(3), Inches(0.45),
        font_name=TITLE_FONT, font_size=16, color=DARK, bold=True,
    )
    _add_textbox(
        slide, "Presentazione riservata — uso interno",
        Inches(0.4), Inches(6.4), Inches(11), Inches(0.4),
        font_size=13, color=RGBColor(0xAA, 0xBB, 0xCC),
        align=PP_ALIGN.CENTER,
    )
    _set_speaker_note(slide, speaker_note)


def add_agenda(prs: Presentation, speaker_note: str = ""):
    slide = _blank_slide(prs)
    _fill_bg(slide, LIGHT)
    _add_rect(slide, 0, 0, Inches(0.18), SLIDE_H, GOLD)
    _add_textbox(slide, "Agenda", Inches(0.5), Inches(0.3), Inches(10), Inches(0.8),
                 font_name=TITLE_FONT, font_size=32, color=NAVY, bold=True)
    items = [
        "1.  Cos'è l'Intelligenza Artificiale",
        "2.  Limiti dell'analisi manuale e ruolo dell'AI",
        "3.  Sei aree di applicazione alla grafologia forense",
        "4.  GraphoLab — i laboratori dimostrativi",
        "5.  Considerazioni etiche e conclusioni",
    ]
    _add_bullet_textbox(slide, items, Inches(0.9), Inches(1.4), Inches(11.5),
                        Inches(5), font_size=22, color=DARK)
    _set_speaker_note(slide, speaker_note)


def add_section(prs: Presentation, number: str, title: str, subtitle: str = "",
                speaker_note: str = ""):
    slide = _blank_slide(prs)
    _fill_bg(slide, NAVY)
    _add_textbox(slide, number, Inches(0.6), Inches(0.5), Inches(2), Inches(1),
                 font_name=TITLE_FONT, font_size=48, color=GOLD, bold=True)
    _add_textbox(slide, title, Inches(0.6), Inches(1.6), Inches(11.5), Inches(2.5),
                 font_name=TITLE_FONT, font_size=40, color=WHITE, bold=True)
    if subtitle:
        _add_textbox(slide, subtitle, Inches(0.6), Inches(4.2), Inches(11.5),
                     Inches(1), font_size=20, color=GOLD, italic=True)
    _add_rect(slide, Inches(0.6), Inches(4.0), Inches(11), Inches(0.06), GOLD)
    _set_speaker_note(slide, speaker_note)


def add_bullet(prs: Presentation, title: str, bullets, note: str = "",
               font_size: int = 19, speaker_note: str = ""):
    slide = _blank_slide(prs)
    _fill_bg(slide, WHITE)
    _add_rect(slide, 0, 0, SLIDE_W, Inches(1.1), NAVY)
    _add_textbox(slide, title, Inches(0.4), Inches(0.15), Inches(12.5), Inches(0.85),
                 font_name=TITLE_FONT, font_size=28, color=WHITE, bold=True)
    _add_rect(slide, 0, Inches(1.1), SLIDE_W, Inches(0.05), GOLD)
    _add_bullet_textbox(slide, bullets, Inches(0.5), Inches(1.35), Inches(12.3),
                        Inches(5.5), font_size=font_size, color=DARK)
    if note:
        _add_rect(slide, 0, SLIDE_H - Inches(0.55), SLIDE_W, Inches(0.55),
                  RGBColor(0xF0, 0xF2, 0xF7))
        _add_textbox(slide, note, Inches(0.4), SLIDE_H - Inches(0.48),
                     Inches(12.5), Inches(0.42), font_size=12, color=GREY, italic=True)
    _set_speaker_note(slide, speaker_note)


def add_bullet_with_image(prs: Presentation, title: str, bullets, img_path,
                          font_size: int = 17, note: str = "", speaker_note: str = ""):
    """Bullet slide with image panel on the right (58% text / 40% image)."""
    slide = _blank_slide(prs)
    _fill_bg(slide, WHITE)
    _add_rect(slide, 0, 0, SLIDE_W, Inches(1.1), NAVY)
    _add_textbox(slide, title, Inches(0.4), Inches(0.15), Inches(12.5), Inches(0.85),
                 font_name=TITLE_FONT, font_size=28, color=WHITE, bold=True)
    _add_rect(slide, 0, Inches(1.1), SLIDE_W, Inches(0.05), GOLD)

    bullet_w = Inches(7.5)
    _add_bullet_textbox(slide, bullets, Inches(0.5), Inches(1.35), bullet_w,
                        Inches(5.5), font_size=font_size, color=DARK)

    # Image panel on the right
    img_left = Inches(8.15)
    img_top  = Inches(1.35)
    img_w    = Inches(4.9)
    img_h    = Inches(5.5)
    _add_rect(slide, img_left - Inches(0.08), img_top,
              img_w + Inches(0.16), img_h, LIGHT)
    _add_picture_fill(slide, img_path, img_left, img_top + Inches(0.2),
                      img_w, img_h - Inches(0.4))

    if note:
        _add_rect(slide, 0, SLIDE_H - Inches(0.55), SLIDE_W, Inches(0.55),
                  RGBColor(0xF0, 0xF2, 0xF7))
        _add_textbox(slide, note, Inches(0.4), SLIDE_H - Inches(0.48),
                     Inches(12.5), Inches(0.42), font_size=12, color=GREY, italic=True)
    _set_speaker_note(slide, speaker_note)


def add_two_col(prs: Presentation, title: str,
                left_title: str, left_bullets,
                right_title: str, right_bullets,
                left_color=NAVY, right_color=RGBColor(0x1A, 0x5C, 0x38),
                left_image=None, speaker_note: str = ""):
    slide = _blank_slide(prs)
    _fill_bg(slide, WHITE)
    _add_rect(slide, 0, 0, SLIDE_W, Inches(1.1), NAVY)
    _add_textbox(slide, title, Inches(0.4), Inches(0.15), Inches(12.5), Inches(0.85),
                 font_name=TITLE_FONT, font_size=28, color=WHITE, bold=True)
    _add_rect(slide, 0, Inches(1.1), SLIDE_W, Inches(0.05), GOLD)

    col_w = Inches(6.0)
    gap   = Inches(0.35)
    top   = Inches(1.35)
    h     = Inches(5.7)

    # Left column
    _add_rect(slide, Inches(0.25), top, col_w, Inches(0.45), left_color)
    _add_textbox(slide, left_title,
                 Inches(0.35), top + Pt(4), col_w - Inches(0.1), Inches(0.4),
                 font_size=16, color=WHITE, bold=True)

    if left_image and Path(left_image).exists():
        # Reduce bullet area and add image below
        bullet_h = Inches(2.8)
        _add_bullet_textbox(slide, left_bullets,
                            Inches(0.35), top + Inches(0.5), col_w - Inches(0.1),
                            bullet_h, font_size=16, color=DARK)
        img_top_l = top + Inches(0.5) + bullet_h + Inches(0.1)
        img_h_l   = h - Inches(0.5) - bullet_h - Inches(0.2)
        _add_rect(slide, Inches(0.25), img_top_l, col_w, img_h_l, LIGHT)
        _add_picture_fill(slide, left_image,
                          Inches(0.35), img_top_l + Inches(0.1),
                          col_w - Inches(0.2), img_h_l - Inches(0.2))
    else:
        _add_bullet_textbox(slide, left_bullets,
                            Inches(0.35), top + Inches(0.5), col_w - Inches(0.1),
                            h - Inches(0.5), font_size=17, color=DARK)

    # Right column
    rx = Inches(0.25) + col_w + gap
    _add_rect(slide, rx, top, col_w, Inches(0.45), right_color)
    _add_textbox(slide, right_title,
                 rx + Inches(0.1), top + Pt(4), col_w - Inches(0.1), Inches(0.4),
                 font_size=16, color=WHITE, bold=True)
    _add_bullet_textbox(slide, right_bullets,
                        rx + Inches(0.1), top + Inches(0.5), col_w - Inches(0.1),
                        h - Inches(0.5), font_size=17, color=DARK)
    _set_speaker_note(slide, speaker_note)


def add_table_slide(prs: Presentation, title: str, headers: list, rows: list,
                    speaker_note: str = ""):
    slide = _blank_slide(prs)
    _fill_bg(slide, WHITE)
    _add_rect(slide, 0, 0, SLIDE_W, Inches(1.1), NAVY)
    _add_textbox(slide, title, Inches(0.4), Inches(0.15), Inches(12.5), Inches(0.85),
                 font_name=TITLE_FONT, font_size=28, color=WHITE, bold=True)
    _add_rect(slide, 0, Inches(1.1), SLIDE_W, Inches(0.05), GOLD)

    n_cols = len(headers)
    n_rows = len(rows) + 1
    tbl_left   = Inches(0.4)
    tbl_top    = Inches(1.35)
    tbl_width  = Inches(12.5)
    tbl_height = Inches(0.48) * n_rows

    table = slide.shapes.add_table(n_rows, n_cols, tbl_left, tbl_top,
                                   tbl_width, tbl_height).table
    col_w = int(tbl_width / n_cols)
    for i in range(n_cols):
        table.columns[i].width = col_w

    def _set_cell(cell, text, bg, fg, bold=False, size=15):
        cell.fill.solid()
        cell.fill.fore_color.rgb = bg
        p = cell.text_frame.paragraphs[0]
        p.alignment = PP_ALIGN.LEFT
        run = p.add_run()
        run.text = text
        run.font.name = BODY_FONT
        run.font.size = Pt(size)
        run.font.color.rgb = fg
        run.font.bold = bold

    for j, h in enumerate(headers):
        _set_cell(table.cell(0, j), h, NAVY, WHITE, bold=True, size=15)
    for i, row in enumerate(rows):
        bg = LIGHT if i % 2 == 0 else WHITE
        for j, val in enumerate(row):
            _set_cell(table.cell(i + 1, j), val, bg, DARK, size=14)
    _set_speaker_note(slide, speaker_note)


# ---------------------------------------------------------------------------
# Presentation content
# ---------------------------------------------------------------------------

def build(prs: Presentation, imgs: dict):

    # === COVER + AGENDA ===
    add_cover(prs, speaker_note="""
Buongiorno a tutti e benvenuti. Oggi vedremo come l'intelligenza artificiale può diventare uno strumento potente nelle mani del perito grafologo e dell'investigatore forense. Non si tratta di sostituire la competenza umana, ma di amplificarla con strumenti che lavorano su larga scala e producono misurazioni oggettive. Iniziamo dall'inizio: cos'è davvero l'intelligenza artificiale?
""")
    add_agenda(prs, speaker_note="""
Ecco la struttura della presentazione. Partiamo dalle basi — cos'è l'AI, come funziona — senza dare nulla per scontato. Poi entriamo nel vivo: i limiti dell'analisi manuale, le sei aree di applicazione, i laboratori pratici. Chiudiamo con le considerazioni etiche e la demo interattiva che potete esplorare direttamente.
""")

    # =========================================================
    # SEZIONE 1 — Cos'è l'Intelligenza Artificiale
    # =========================================================
    add_section(prs, "01", "Cos'è l'Intelligenza Artificiale",
                "Una introduzione accessibile per professionisti non tecnici",
                speaker_note="""
Partiamo dalle fondamenta. Questo capitolo è dedicato a chi non ha background tecnico: spiegheremo l'AI con analogie concrete, senza formule matematiche. L'obiettivo è costruire un vocabolario comune che ci servirà per il resto della presentazione.
""")

    add_bullet(prs, "Cos'è l'Intelligenza Artificiale", [
        "L'intelligenza artificiale (AI) è la capacità di un sistema informatico di svolgere compiti che normalmente richiedono intelligenza umana",
        "Non si tratta di un singolo algoritmo, ma di una famiglia di tecniche: machine learning, reti neurali, computer vision, elaborazione del linguaggio naturale",
        "L'AI non «pensa» come un essere umano — riconosce pattern statistici in grandi quantità di dati",
        "Il risultato è un sistema capace di generalizzare: applicare ciò che ha imparato a casi nuovi, mai visti prima",
    ], note="AI = riconoscimento di pattern su larga scala, non intelligenza nel senso umano del termine",
    speaker_note="""
La prima cosa da chiarire: l'AI non è magia, non è fantascienza e non 'pensa' nel senso umano. È un sistema statistico che ha visto milioni di esempi e ha imparato a riconoscere pattern. Pensate a come un bambino impara a riconoscere un gatto: vede mille gatti diversi finché il suo cervello costruisce un'idea generale di 'gatto'. L'AI fa esattamente questo, ma con dati numerici e su scala enormemente maggiore.
""")

    add_bullet(prs, "AI vs. Regole Manuali: Programmare vs. Imparare", [
        "Approccio tradizionale: un programmatore scrive regole esplicite  →  il computer le esegue",
        "  Esempio: «Se il pixel è scuro E la forma è tonda, allora è la lettera O»",
        "Approccio AI (machine learning): il sistema riceve migliaia di esempi etichettati e deduce autonomamente le regole",
        "  Esempio: il modello vede 100.000 lettere «O» scritte a mano e impara da solo come riconoscerle",
        "Vantaggio: i sistemi AI gestiscono variabilità e complessità che sarebbe impossibile codificare manualmente",
    ], speaker_note="""
L'approccio tradizionale è: un programmatore scrive tutte le regole a mano. Ma la scrittura a mano è infinitamente variabile: impossibile scrivere una regola per ogni stile di ogni persona. Il machine learning capovolge l'approccio: invece di scrivere le regole, diamo al sistema migliaia di esempi etichettati e lui le deduce da solo. Questo è il salto concettuale fondamentale dell'AI moderna.
""")

    add_bullet_with_image(prs, "Come Impara una Macchina: il Training",
        [
            "Training: il modello analizza esempi e aggiusta i propri parametri interni",
            "Fase 1 — Input: dati etichettati (es. firme con etichetta «autentica» o «falsa»)",
            "Fase 2 — Previsione: il modello produce un output con score di confidenza",
            "Fase 3 — Errore: si misura quanto la previsione si discosta dall'etichetta reale",
            "Fase 4 — Aggiornamento: i parametri vengono modificati per ridurre l'errore",
            "Il ciclo si ripete su milioni di esempi fino a convergenza",
        ],
        imgs.get("training_cycle"),
        note="Il training avviene una sola volta. L'uso del modello addestrato (inferenza) è istantaneo.",
        speaker_note="""
Il diagramma mostra il ciclo di training. È come un bambino che impara con le flash card: vede l'esempio, fa un tentativo, riceve una correzione, aggiusta. Questo ciclo si ripete milioni di volte. La cosa importante da capire: il training avviene una volta sola — può richiedere ore o giorni su hardware specializzato. Dopo, usare il modello addestrato è istantaneo, come caricare un esperto virtuale che ha già studiato tutto.
""")

    add_bullet(prs, "I Dati di Addestramento: il Carburante dell'AI", [
        "Senza dati non c'è apprendimento: la qualità del modello dipende direttamente dalla qualità e quantità dei dati di training",
        "Dataset tipici per grafologia forense: migliaia di firme etichettate, campioni di scrittura da più scrittori, documenti scansionati",
        "Bias nei dati: se il dataset è sbilanciato, il modello potrebbe funzionare peggio su gruppi sotto-rappresentati",
        "I modelli usati in GraphoLab sono addestrati su dataset pubblici di riferimento: GPDS (firme), IAM (scrittura), CEDAR (firme)",
    ], speaker_note="""
Si dice 'garbage in, garbage out': se il dataset è sbilanciato o di bassa qualità, il modello sarà sbilanciato. Per la grafologia forense questo è critico: un modello addestrato solo su firme di un certo periodo o cultura potrebbe funzionare meno bene su altri. I modelli di GraphoLab usano dataset pubblici di riferimento — GPDS, IAM, CEDAR — che sono gli standard del settore, con migliaia di campioni verificati.
""")

    add_bullet_with_image(prs, "Reti Neurali: Ispirazione Biologica",
        [
            "Le reti neurali artificiali sono ispirate al funzionamento del cervello: unità di calcolo (neuroni) collegate in strati",
            "Ogni connessione ha un peso numerico aggiustato durante il training",
            "Strato di input  →  strati nascosti (elaborazione)  →  strato di output",
            "Le reti imparano rappresentazioni gerarchiche: dai tratti alle lettere, alle parole",
            "Esempio: una rete per le firme impara prima a riconoscere bordi e curve, poi pattern stilistici",
        ],
        imgs.get("neural_network"),
        speaker_note="""
Le reti neurali sono ispirate al cervello, ma non antropomorfizzatele troppo. Sono sistemi matematici con milioni di parametri numerici aggiustati durante il training. Il concetto chiave è la gerarchia: i primi strati riconoscono elementi semplici come bordi, i successivi riconoscono forme più complesse, i finali riconoscono strutture ad alto livello. Per le firme: prima bordi e curve, poi loop e tratti caratteristici, poi lo stile complessivo dello scrittore.
""")

    add_bullet_with_image(prs, "Computer Vision: Vedere con le Macchine",
        [
            "La computer vision insegna alle macchine a interpretare immagini",
            "Un'immagine è una griglia di pixel — valori numerici di intensità luminosa",
            "Le reti convoluzionali (CNN) applicano filtri che rilevano bordi, texture e forme a scale diverse",
            "In grafologia forense, la computer vision è lo strumento per:",
            "  Leggere testo manoscritto (HTR)",
            "  Rilevare e verificare firme",
            "  Misurare caratteristiche grafologiche",
        ],
        imgs.get("computer_vision"),
        speaker_note="""
Per noi un'immagine è qualcosa di visivo e significativo. Per la macchina è una griglia di numeri: valori da 0 a 255 per ogni pixel. I filtri convoluzionali scorrono su questa griglia cercando pattern — bordi, angoli, texture. Questi pattern vengono combinati gerarchicamente fino a produrre una classificazione. In grafologia, questo significa che la macchina può misurare l'angolo di ogni tratto con precisione che nessun occhio umano potrebbe raggiungere in modo consistente.
""")

    add_bullet_with_image(prs, "Output Probabilistici: Score, Non Verdetti",
        [
            "I sistemi AI non producono risposte binarie assolute — producono distribuzioni di probabilità",
            "Esempio firma: «Autentica con confidenza 91%» oppure «Falsa con confidenza 85%»",
            "Una soglia decisionale converte la probabilità in una classificazione",
            "La soglia deve essere calibrata sul tipo di rischio accettabile nel contesto forense",
            "Regola fondamentale: lo score AI è un dato quantitativo — l'interpretazione spetta all'esaminatore",
        ],
        imgs.get("confidence_scores"),
        note="Un sistema AI non 'sa' se una firma è falsa. Sa che la distanza coseno è 0.62, che supera la soglia 0.35.",
        speaker_note="""
Questo è un punto cruciale da comunicare bene, soprattutto in sede giudiziaria. L'AI non dice 'questa firma è falsa'. Dice 'la distanza coseno tra questa firma e il riferimento è 0.62, che supera la soglia calibrata di 0.35'. Tutto il resto — interpretare questo numero nel contesto del caso specifico — spetta all'esaminatore. La soglia può essere regolata a seconda del livello di rischio accettabile: una banca e un tribunale penale potrebbero calibrarla diversamente.
""")

    add_bullet(prs, "AI nella Vita Quotidiana: Esempi Familiari", [
        "Traduzione automatica (Google Translate, DeepL) — comprensione del linguaggio naturale",
        "Filtro antispam delle email — classificazione testi",
        "Riconoscimento vocale (Siri, Alexa) — conversione audio → testo",
        "Face ID sullo smartphone — riconoscimento facciale",
        "Diagnosi medica da immagini radiologiche — computer vision applicata alla medicina",
        "In tutti questi casi: l'AI non «capisce», ma ha imparato a riconoscere pattern statistici con alta affidabilità",
    ], speaker_note="""
Per dare concretezza, ricordiamo che tutti usiamo l'AI ogni giorno senza saperlo: il filtro spam della posta, la traduzione automatica, il riconoscimento facciale del telefono. Questi sono tutti esempi dello stesso principio: riconoscimento di pattern su grandi quantità di dati. La differenza rispetto all'uso forense è che in forensics dobbiamo documentare ogni passo e spiegarlo in modo comprensibile a giudici e giurie.
""")

    add_bullet(prs, "Cosa l'AI Non È", [
        "Non è infallibile: i modelli commettono errori, specialmente su casi anomali o dati degradati",
        "Non è trasparente per default: i modelli deep learning sono «scatole nere»",
        "Non sostituisce l'esperienza: non ha accesso al contesto del caso, alla storia del documento",
        "Non produce certezze legali: uno score al 95% non equivale a prova oltre ogni ragionevole dubbio",
        "Non è neutrale per default: riflette i bias presenti nei dati di addestramento",
        "L'AI è uno strumento potente — come un microscopio: trasforma il lavoro dell'esperto, non lo elimina",
    ], note="Il valore dell'AI in ambito forense dipende dal modo in cui viene integrata nel processo peritale.",
    speaker_note="""
Questo è forse il punto più importante. L'AI non è infallibile — commette errori, specialmente su casi ai margini della distribuzione su cui è stata addestrata. Non è trasparente: i modelli deep learning sono 'scatole nere' e spiegare le loro decisioni richiede strumenti appositi. Non ha accesso al contesto del caso. Non produce certezze legali: un 95% di confidenza non è 'prova oltre ogni ragionevole dubbio'. È uno strumento — potente come un microscopio, ma il microscopio non emette diagnosi. Questo porta naturalmente alla domanda: dove l'analisi manuale mostra i suoi limiti?
""")

    # =========================================================
    # SEZIONE 2 — Limiti del metodo manuale e ruolo dell'AI
    # =========================================================
    add_section(prs, "02", "Limiti dell'Analisi Manuale\ne Ruolo dell'AI",
                "Dove l'AI aggiunge valore reale al lavoro dell'esaminatore forense",
                speaker_note="""
Passiamo alla domanda pratica: perché un perito esperto dovrebbe voler usare l'AI? Non per moda o innovazione — ma perché esistono limiti strutturali dell'analisi puramente manuale che l'AI può superare. Vediamoli uno per uno.
""")

    add_bullet(prs, "Limiti dell'Analisi Manuale: Tempo e Scalabilità", [
        "Il confronto manuale di firme o campioni di scrittura è un processo lento e meticoloso",
        "Un esaminatore esperto può analizzare in dettaglio pochi campioni al giorno",
        "Nelle indagini complesse — contratti multi-pagina, archivi finanziari — il volume supera la capacità manuale",
        "Esempio pratico: screening di 10.000 pagine per individuare firme anomale  →  settimane di lavoro manuale vs. poche ore con AI",
        "L'AI non si stanca, non si distrae e applica gli stessi criteri a ogni documento",
    ], speaker_note="""
Immaginate di dover esaminare 50.000 pagine di documentazione bancaria per trovare firme anomale. Un perito esperto, lavorando con rigore, può analizzare qualche decina di firme al giorno. L'AI può pre-filtrare questo archivio in poche ore, segnalando i casi sospetti per la revisione umana. Il perito si concentra sull'1-2% critico, non sull'altro 98%. Questo non riduce il ruolo del perito — lo rende molto più efficace.
""")

    add_bullet(prs, "Limiti dell'Analisi Manuale: Soggettività e Riproducibilità", [
        "L'analisi visiva è intrinsecamente soggettiva: due periti esperti possono giungere a conclusioni diverse sullo stesso campione borderline",
        "Non si tratta di incompetenza — è una caratteristica strutturale di qualsiasi valutazione basata sul giudizio visivo",
        "I tribunali richiedono metodologie verificabili e riproducibili da terze parti",
        "L'AI produce valori numerici precisi (es. inclinazione media 11.3°, spaziatura 4.2 mm) verificabili indipendentemente",
        "Questo non elimina il giudizio del perito, ma lo ancora a misurazioni oggettive",
    ], speaker_note="""
Non è una critica ai periti — è una caratteristica strutturale del giudizio visivo umano. Due radiologi esperti non vedono esattamente la stessa cosa guardando la stessa radiografia. Due sommelier esperti non danno lo stesso voto allo stesso vino. La differenza in forensics è che i tribunali chiedono metodologie verificabili e riproducibili da terze parti. Quando l'AI dice 'inclinazione media 11.3 gradi', questa misurazione è verificabile indipendentemente da chiunque con lo stesso strumento.
""")

    add_bullet(prs, "Limiti dell'Analisi Manuale: Scala degli Archivi Digitali", [
        "La digitalizzazione ha trasformato le controversie legali: i casi coinvolgono sempre più grandi archivi di documenti scansionati",
        "Esaminare manualmente 50.000 pagine di documentazione bancaria non è fattibile nei tempi processuali",
        "L'AI può pre-filtrare: individuare automaticamente le pagine con firme, segnalare anomalie, produrre priorità",
        "L'esaminatore si concentra poi sui casi segnalati come sospetti — moltiplicando l'efficacia del suo lavoro",
    ], speaker_note="""
I processi moderni — frodi finanziarie, successioni contestate, falsi documentali — coinvolgono sempre più archivi digitali enormi. Non è realistico aspettarsi che l'analisi manuale tenga il passo con il volume di documenti nelle controversie commerciali e finanziarie contemporanee. L'AI cambia il modello operativo: invece di 'esamina tutto manualmente', diventa 'AI pre-filtra, esperto verifica i casi segnalati'. Il risultato è un uso molto più efficiente del tempo del perito.
""")

    add_bullet(prs, "Cosa l'AI Aggiunge: Misurazioni Oggettive e Audit Trail", [
        "Misurazioni quantitative: l'AI produce numeri, non impressioni — angoli, distanze, score di similarità",
        "Riproducibilità: chiunque esegua lo stesso modello sullo stesso file ottiene lo stesso risultato",
        "Audit trail: ogni passo dell'analisi AI può essere registrato e documentato",
        "Il referto peritale supportato da misurazioni AI è più resistente alla contestazione tecnica in sede processuale",
        "Standard internazionali (OSAC, SWGDOC) stanno integrando le linee guida per gli strumenti computazionali",
    ], speaker_note="""
Tre parole chiave: numeri, riproducibilità, documentazione. L'AI non dice 'questa firma sembra diversa'. Dice 'distanza coseno = 0.62'. Chiunque riesegua l'analisi con lo stesso modello e lo stesso file ottiene lo stesso numero — zero margine di interpretazione soggettiva nella misurazione. Ogni passo è loggato con timestamp, versione del modello e parametri. Un referto peritale supportato da questi dati è molto più resistente alla contestazione tecnica in aula.
""")

    add_bullet(prs, "Modello di Collaborazione Uomo-AI", [
        "L'AI non è un sostituto dell'esaminatore forense — è un amplificatore delle sue capacità",
        "Cosa fa l'AI:",
        "  Elabora grandi volumi di documenti in tempi brevi",
        "  Produce misurazioni oggettive e riproducibili",
        "  Segnala anomalie e casi sospetti per la revisione umana",
        "Cosa fa l'esaminatore:",
        "  Interpreta i risultati nel contesto del caso",
        "  Applica conoscenza peritale specializzata",
        "  Produce un'opinione professionale con responsabilità legale",
    ], note="Il modello ideale: AI per la quantificazione, esperto per l'interpretazione.",
    speaker_note="""
Il messaggio centrale è questo: l'AI fa il lavoro quantitativo su larga scala — misurazioni, screening, prioritizzazione. L'esperto porta ciò che la macchina non può: contesto del caso, conoscenza specializzata delle dinamiche della scrittura, giudizio professionale, responsabilità legale. Non è competizione, è specializzazione. Passiamo ora a vedere le sei aree concrete dove questo modello si applica.
""")

    # =========================================================
    # SEZIONE 3 — Sei Aree di Applicazione
    # =========================================================
    add_section(prs, "03", "Sei Aree di Applicazione\nalla Grafologia Forense",
                "Da HTR alla NER: la pipeline AI per l'esame dei documenti",
                speaker_note="""
Passiamo alle applicazioni concrete. Vi mostrerò sei aree dove l'AI è già utilizzabile oggi nella pratica forense — non in modo sperimentale, ma con strumenti stabili, documentati e con dataset di riferimento consolidati.
""")

    # 1. HTR
    add_two_col(prs,
        "1. Trascrizione del Testo Manoscritto (HTR)",
        "Come funziona",
        [
            "HTR = Handwritten Text Recognition",
            "TrOCR: encoder visivo BEiT + decoder testuale RoBERTa",
            "L'immagine viene convertita in sequenza di token",
            "Documenti multi-riga: ogni riga viene segmentata e trascritta separatamente",
            "Output: testo digitale ricercabile e processabile",
        ],
        "Applicazioni forensi",
        [
            "Trascrizione automatica di lettere anonime o minatorie",
            "Digitalizzazione di atti giudiziari storici manoscritti",
            "Pre-elaborazione per pipeline di identificazione dell'autore",
            "Calcolo automatico del Character Error Rate (CER)",
        ],
        speaker_note="""
HTR significa Handwritten Text Recognition: il sistema legge la scrittura a mano come farebbe un dattilografo molto veloce e molto paziente. Il modello TrOCR è addestrato su milioni di esempi di testo manoscritto. Non interpreta il significato, non giudica la calligrafia — converte l'immagine in testo digitale ricercabile. Per una lettera anonima, questo significa: in pochi secondi avete un documento cercabile per parole chiave. Tecnicamente: BEiT è l'encoder visivo che 'vede' l'immagine, RoBERTa è il decoder linguistico che produce il testo.
""")

    add_bullet(prs, "1. HTR — Casi d'Uso Forensi: Esempi Concreti", [
        "Caso: lettera anonima con minacce estorsive — l'HTR produce in secondi una trascrizione completa e ricercabile",
        "Caso: testamento olografo storico in grafia difficile — l'HTR supporta la lettura e la digitalizzazione",
        "Limite: l'HTR non interpreta il contenuto, non attribuisce l'autore",
        "Il testo trascritto diventa l'input per le fasi successive della pipeline:",
        "  Ricerca di pattern linguistici",
        "  Estrazione di entità nominate (NER)",
        "  Analisi stilistica per identificazione dell'autore",
    ], note="Modello: microsoft/trocr-base-handwritten  |  Supporto: testo scritto a mano in lingue romanze",
    speaker_note="""
Un esempio concreto: una lettera minatoria scritta a mano arriva in ufficio. Il sistema trascrive il testo in secondi. Il testo trascritto viene poi passato al modulo NER per estrarre nomi, luoghi, organizzazioni — senza leggere manualmente ogni parola. Questo è il potere della pipeline: ogni strumento fa il suo pezzo, il risultato finale è molto più della somma delle parti. Il limite principale: l'HTR trascrive quello che vede, possono esserci errori su calligrafia molto personale o degradata.
""")

    # 2. Verifica firma — with siamese diagram in left column
    add_two_col(prs,
        "2. Verifica dell'Autenticità della Firma",
        "Come funziona",
        [
            "SigNet: rete siamese addestrata su dataset GPDS",
            "I due rami elaborano firma di riferimento e firma in esame",
            "Ogni firma → vettore di caratteristiche (embedding)",
            "Distanza coseno > 0.35 → classificata come FALSA",
        ],
        "Applicazioni forensi",
        [
            "Verifica di firme su assegni bancari",
            "Autenticazione di firme su contratti, testamenti, atti notarili",
            "Rilevamento di firme tracciate o riprodotte digitalmente",
            "Confronto sistematico di grandi lotti di firme",
            "Output: score numerico per il perito — non un verdetto",
        ],
        left_image=imgs.get("siamese_network"),
        speaker_note="""
La rete siamese è elegante nella sua architettura: due rami identici elaborano la firma di riferimento e quella in esame, producendo per ognuna un vettore di caratteristiche nello stesso spazio matematico. Si misura poi la distanza tra questi vettori. Se la distanza è grande, le firme sono stilisticamente diverse. Il modello è addestrato su migliaia di coppie firma-autentica/firma-falsa dal dataset GPDS. Non riconosce 'questa è la firma di Mario' — riconosce pattern di tratti, proporzioni, velocità.
""")

    add_bullet(prs, "2. Verifica Firma — Note Tecniche per l'Esaminatore", [
        "Il modello SigNet funziona meglio su firme isolate, su sfondo bianco, ad alta risoluzione",
        "Limite principale: le «skilled forgeries» (imitatori abili) possono ingannare il modello",
        "I campioni demo sono pre-selezionati dal database CEDAR: il modello rileva correttamente la contraffazione",
        "Interpretazione: più alta è la distanza coseno, più probabile è la contraffazione",
        "In casi borderline (distanza vicina alla soglia): necessaria revisione manuale dell'esaminatore",
    ], note="Modello: SigNet (luizgh/sigver) — pesi pre-addestrati su GPDS, campioni demo da CEDAR",
    speaker_note="""
Alcune precisazioni importanti per l'uso forense. Il modello funziona meglio su firme isolate, ad alta risoluzione, su sfondo chiaro. Le 'skilled forgeries' — imitatori molto abili che hanno esercitato a lungo la firma del soggetto — possono ingannare il modello, proprio come possono ingannare un esaminatore. Per questo il risultato va sempre interpretato dall'esaminatore nel contesto del caso: chi sono i possibili falsari? Avevano accesso a campioni della firma? Il sistema fornisce un dato quantitativo — il giudizio rimane umano.
""")

    # 3. Rilevamento firma — with DETR diagram
    add_two_col(prs,
        "3. Rilevamento Automatico di Firme nei Documenti",
        "Come funziona",
        [
            "Conditional DETR: Transformer encoder-decoder per rilevamento di oggetti",
            "Fine-tuned su dataset di documenti con firme annotate",
            "Produce bounding box con score di confidenza",
            "Gestisce pagine con firme multiple o parzialmente coperte",
        ],
        "Applicazioni forensi",
        [
            "Estrazione automatica di firme da contratti multi-pagina",
            "Screening di grandi archivi documentali",
            "Pipeline: Rileva → Estrai → Verifica con SigNet",
            "Report con posizione e confidenza di ogni firma",
        ],
        left_image=imgs.get("detr_detection"),
        speaker_note="""
Conditional DETR è un modello Transformer per il rilevamento di oggetti basato su ResNet-50 come backbone e un decoder Transformer per la predizione dei bounding box. Qui viene fine-tuned specificamente per trovare firme nei documenti con mAP@50 del 93,65%. La pipeline naturale è: trova le firme nel documento con Conditional DETR, poi passa ognuna a SigNet per verificarne l'autenticità. Questo è lo screening automatico di un intero contratto multi-pagina in pochi secondi.
""")

    # 4. Identificazione scrittore
    add_two_col(prs,
        "4. Identificazione dello Scrittore",
        "Come funziona",
        [
            "Feature extraction: HOG (Histogram of Oriented Gradients)",
            "Cattura stile, ritmo e distribuzione della scrittura",
            "Classificatore SVM addestrato sui campioni di riferimento",
            "Output: lista ordinata di autori candidati con score",
            "Validazione: cross-validation leave-one-out",
        ],
        "Applicazioni forensi",
        [
            "Attribuzione di lettere minatorie o estorsive anonime",
            "Verifica della paternità di documenti contestati",
            "Ricerca sulla provenienza di manoscritti storici",
            "Confronto tra campione anonimo e archivio di riferimento",
        ],
        speaker_note="""
Il sistema estrae le caratteristiche stilistiche della scrittura: HOG cattura come i tratti sono distribuiti e orientati nello spazio, LBP cattura le texture locali del tratto, le statistiche di run-length catturano la regolarità. L'SVM impara a distinguere lo stile di scrittore A da quello di scrittore B. Il risultato è una classifica di probabilità — non un verdetto di autoria, ma una lista di candidati su cui concentrare l'esame peritale. È il primo filtro quantitativo in un'indagine di attribuzione.
""")

    # 5. Analisi grafologica
    add_two_col(prs,
        "5. Analisi delle Caratteristiche Grafologiche",
        "Come funziona",
        [
            "Computer vision classica (OpenCV) per segmentazione",
            "Pipeline: binarizzazione → de-noise → raddrizzamento",
            "Misurazioni estratte:",
            "  Angolo di inclinazione delle lettere",
            "  Spaziatura tra parole e caratteri",
            "  Altezza e larghezza media dei caratteri",
            "  Pressione del tratto (intensità pixel)",
        ],
        "Applicazioni forensi",
        [
            "Supporto alla testimonianza peritale con misurazioni oggettive",
            "Confronto tra campione di riferimento e documento contestato",
            "Rilevamento di variazioni anomale: stress, camouflage, mano diversa",
            "Dashboard visiva: metriche con annotazioni sull'immagine originale",
        ],
        speaker_note="""
Questo è il modulo più vicino al lavoro tradizionale del perito grafologo: misurare le caratteristiche della scrittura. La differenza è che la macchina le misura con precisione numerica e in modo identico su ogni campione. Un perito esperto potrebbe dire 'questa scrittura ha un'inclinazione destra moderata'. Il sistema dice 'inclinazione media 12.3 gradi, deviazione standard 2.1 gradi'. Queste misurazioni sono direttamente comparabili tra campioni diversi e documentabili nel referto.
""")

    # 6. NER
    add_two_col(prs,
        "6. Riconoscimento delle Entità Nominate (NER)",
        "Come funziona",
        [
            "NER: classificazione automatica di entità nel testo",
            "WikiNEural: BERT multilingue, addestrato su Wikipedia in 9 lingue",
            "Schema BIO: B-PER, I-PER, B-ORG, B-LOC, B-MISC, O",
            "Output: lista di entità con tipo, testo e confidenza",
        ],
        "Applicazioni forensi",
        [
            "Identificare persone, luoghi, organizzazioni in lettere anonime",
            "Analizzare testamenti per estrarre nomi di eredi e beni",
            "Costruire un grafo delle relazioni tra entità",
            "Screening di archivi per la presenza di nomi specifici",
            "Pipeline completa: immagine → HTR → NER → grafo relazioni",
        ],
        speaker_note="""
NER significa Named Entity Recognition: il sistema legge il testo e identifica automaticamente le entità nominate — persone, organizzazioni, luoghi. Il modello WikiNEural è addestrato su Wikipedia in 9 lingue, quindi funziona bene su testi italiani senza configurazione aggiuntiva. È particolarmente potente come secondo passo dopo la trascrizione HTR: dall'immagine manoscritto alle entità strutturate in pochi secondi.
""")

    add_bullet(prs, "6. NER — Esempio di Applicazione Forense", [
        "Testo trascritto da HTR (testamento olografo):",
        "  «Io sottoscritto Mario Rossi, nato a Roma il 12 marzo 1950, nomino mio erede Giovanni Costa»",
        "Output NER:",
        "  [Persona] Mario Rossi          (confidenza 99%)",
        "  [Luogo]   Roma                 (confidenza 98%)",
        "  [Persona] Giovanni Costa       (confidenza 97%)",
        "Applicazione: costruzione automatica del grafo ereditario da documenti multipli",
        "Lingue supportate: italiano, inglese, tedesco, spagnolo, francese, russo, olandese, polacco, cinese",
    ], note="Modello: Babelscape/wikineural-multilingual-ner  |  Supporto multilingue nativo",
    speaker_note="""
Questo è l'esempio più concreto della pipeline completa. Un testamento olografo viene fotografato, TrOCR lo trascrive, WikiNEural estrae le entità. In pochi secondi avete una lista strutturata di tutte le persone, i luoghi e le organizzazioni menzionati nel documento — senza leggere manualmente ogni parola. Questo diventa ancora più potente quando avete decine di documenti: il sistema costruisce automaticamente un grafo delle relazioni tra tutti i soggetti citati nell'intero archivio.
""")

    # =========================================================
    # SEZIONE 4 — GraphoLab: i Laboratori
    # =========================================================
    add_section(prs, "04", "GraphoLab — I Laboratori",
                "Sette laboratori dimostrativi e un'applicazione interattiva",
                speaker_note="""
Ora vediamo concretamente GraphoLab: i sette laboratori dimostrativi e la demo interattiva. Tutto il codice è aperto, documentato e può essere eseguito localmente o via Docker senza configurazione complessa.
""")

    add_table_slide(prs,
        "GraphoLab: Panoramica dei 7 Laboratori",
        ["Lab", "Titolo", "Tecnica AI", "Modello / Strumento"],
        [
            ["01", "Introduzione", "—", "Panoramica concettuale"],
            ["02", "Riconoscimento testo manoscritto", "Transformer OCR", "TrOCR (microsoft)"],
            ["03", "Verifica autenticità firma", "Siamese Network", "SigNet (luizgh/sigver)"],
            ["04", "Rilevamento firma nei documenti", "Object Detection", "Conditional DETR (tech4humans)"],
            ["05", "Identificazione dello scrittore", "HOG + SVM", "scikit-learn"],
            ["06", "Analisi caratteristiche grafologiche", "Image Processing", "OpenCV"],
            ["07", "Riconoscimento entità nominate", "Token Classification", "WikiNEural (Babelscape)"],
        ],
        speaker_note="""
Ecco la mappa completa dei sette lab. Ognuno è autonomo — potete eseguirli nell'ordine che preferite. Il Lab 01 non ha codice: è una presentazione concettuale pensata per questo tipo di pubblico. Dal Lab 02 in poi c'è codice funzionante, dati di esempio inclusi, e una sezione dedicata ai casi d'uso forensi. I modelli scaricano automaticamente da Hugging Face al primo avvio.
""")

    add_bullet(prs, "Lab 01 — Introduzione: AI e Grafologia Forense", [
        "Notebook in stile presentazione, senza codice eseguibile",
        "Destinato a tutti i livelli — inclusi stakeholder non tecnici",
        "Contenuti:",
        "  Definizione di grafologia forense e ambiti di applicazione",
        "  Mappa concettuale della pipeline: acquisizione → preprocessing → AI → referto",
        "  Panoramica dei laboratori successivi",
    ], note="File: notebooks/01_intro_forensic_graphology.ipynb",
    speaker_note="""
Il Lab 01 è pensato proprio per chi è in sala oggi: una presentazione interattiva che spiega i concetti senza richiedere competenze di programmazione. È anche ottimo materiale da condividere con clienti, colleghi magistrati o consulenti tecnici di parte che vogliono capire cosa fa l'AI in questo contesto, senza dover eseguire codice.
""")

    add_bullet_with_image(prs, "Lab 02 — Riconoscimento del Testo Manoscritto",
        [
            "Modello: TrOCR (microsoft/trocr-base-handwritten) via Hugging Face",
            "Architettura: encoder visivo BEiT + decoder testuale RoBERTa",
            "Demo:",
            "  Trascrizione di testo manoscritto su riga singola",
            "  Documento multi-riga con segmentazione automatica",
            "  Calcolo opzionale del Character Error Rate (CER)",
        ],
        imgs.get("handwritten"),
        note="File: notebooks/02_handwritten_ocr_trocr.ipynb  |  Prerequisiti: transformers, torch, Pillow",
        font_size=17,
        speaker_note="""
TrOCR è il modello di punta di Microsoft per il riconoscimento del testo manoscritto, disponibile gratuitamente su Hugging Face. Scarica automaticamente al primo avvio — circa 350 MB. Il lab include tre demo: testo su riga singola, documento multi-riga con segmentazione automatica delle righe, e calcolo del CER per valutare l'accuratezza su un testo di riferimento noto.
""")

    add_bullet_with_image(prs, "Lab 03 — Verifica dell'Autenticità della Firma",
        [
            "Modello: SigNet con pesi pre-addestrati su dataset GPDS",
            "Demo:",
            "  Caricamento firma di riferimento e firma in esame",
            "  Estrazione embedding con il codificatore SigNet",
            "  Calcolo score di similarità coseno",
            "  Verdetto: AUTENTICA / FALSA + score di confidenza",
            "Campioni demo pre-selezionati dal database CEDAR",
        ],
        imgs.get("signatures_comparison"),
        note="File: notebooks/03_signature_verification_siamese.ipynb  |  Richiede: models/signet.pth",
        font_size=17,
        speaker_note="""
Questo è il cuore del sistema per la verifica delle firme. SigNet è il modello standard nel settore — i suoi pesi pre-addestrati sono disponibili pubblicamente nel repository luizgh/sigver. I campioni di demo provengono dal database CEDAR e sono pre-selezionati per mostrare sia casi di firma autentica che di contraffazione chiaramente rilevabile. Nella demo Gradio potete caricare le vostre firme e vedere il risultato in tempo reale.
""")

    add_bullet(prs, "Lab 04 — Rilevamento Firma nei Documenti", [
        "Modello: Conditional DETR fine-tuned (tech4humans/conditional-detr-50-signature-detector)",
        "Modello pubblico su Hugging Face (Apache 2.0) — nessun token necessario",
        "Demo:",
        "  Caricamento di un documento scansionato",
        "  Inferenza Conditional DETR: localizzazione firme con bounding box",
        "  Ritaglio automatico di ogni firma → input per Lab 03",
        "Pipeline dimostrativa: Rileva → Estrai → Verifica",
    ], note="File: notebooks/04_signature_detection_detr.ipynb  |  Prerequisiti: transformers, timm, opencv-python",
    speaker_note="""
Conditional DETR è disponibile pubblicamente su Hugging Face senza token di autenticazione, con licenza Apache 2.0. Il lab mostra la pipeline completa: il documento entra, le firme vengono rilevate e ritagliate, ogni ritaglio viene passato a SigNet per la verifica. Non è richiesta alcuna configurazione aggiuntiva oltre all'installazione di transformers e timm.
""")

    add_bullet(prs, "Lab 05 — Identificazione dello Scrittore", [
        "Tecnica: estrazione feature HOG + classificatore SVM (scikit-learn)",
        "Demo:",
        "  Caricamento di campioni di scrittori noti (dataset IAM subset)",
        "  Estrazione delle feature stilistiche da ogni campione",
        "  Addestramento SVM con cross-validation leave-one-out",
        "  Input anonimo → lista ordinata di autori candidati con score",
    ], note="File: notebooks/05_writer_identification.ipynb  |  Prerequisiti: torch, scikit-learn",
    speaker_note="""
Il lab sull'identificazione dello scrittore usa scikit-learn — nessuna GPU necessaria, gira su qualsiasi laptop. Il database di esempio include cinque scrittori con stili diversi. Per un uso forense reale, sostituite questo database con campioni autentici degli scrittori candidati: il sistema si riaddestra automaticamente. L'output è una classifica di probabilità — un punto di partenza quantitativo per l'indagine peritale.
""")

    add_bullet(prs, "Lab 06 — Analisi delle Caratteristiche Grafologiche", [
        "Strumenti: OpenCV + numpy + scipy",
        "Pipeline: binarizzazione (Otsu) → de-noise → raddrizzamento",
        "Metriche estratte e visualizzate:",
        "  Angolo di inclinazione delle lettere",
        "  Spaziatura media tra parole e caratteri",
        "  Distribuzione dell'intensità dei pixel (stima pressione tratto)",
        "Confronto opzionale affiancato tra due campioni",
    ], note="File: notebooks/06_graphological_feature_analysis.ipynb  |  Prerequisiti: opencv-python, scipy",
    speaker_note="""
L'analisi grafologica usa OpenCV — libreria di computer vision classica, stabile, documentata e gratuita. Non richiede GPU. La pipeline produce una dashboard visiva con le metriche annotate sull'immagine originale — ottimo per la presentazione in tribunale o per il referto peritale. Il confronto affiancato tra due campioni rende immediatamente visibili le differenze nelle caratteristiche misurate.
""")

    add_bullet(prs, "Lab 07 — Riconoscimento delle Entità Nominate (NER)", [
        "Modello: Babelscape/wikineural-multilingual-ner (BERT, 9 lingue)",
        "Demo 1: NER su testo italiano (testamento / dichiarazione giurata)",
        "Demo 2: NER su testo inglese (verifica supporto multilingue)",
        "Demo 3: pipeline completa HTR → NER (immagine → entità)",
        "Demo 4: distribuzione delle entità e analisi della confidenza",
        "Output: testo con evidenziazione colorata + tabella riepilogativa",
    ], note="File: notebooks/07_named_entity_recognition.ipynb  |  Prerequisiti: transformers, torch",
    speaker_note="""
Il Lab 07 è il più potente per l'analisi di documenti testuali. Il modello WikiNEural funziona su 9 lingue senza configurazione aggiuntiva — italiano, inglese, tedesco, spagnolo e altre. La Demo 3 è la più significativa: parte da un'immagine di testo manoscritto e arriva alle entità nominate estratte, tutto in un unico flusso automatico. L'output con evidenziazione colorata è direttamente utilizzabile in un referto.
""")

    add_bullet(prs, "Demo Interattiva Gradio — 6 Tab in Italiano", [
        "Applicazione web: python app/grapholab_demo.py  →  http://localhost:7860",
        "Tab 1 — OCR Manoscritto: carica immagine → testo trascritto (supporto multi-riga)",
        "Tab 2 — Verifica Firma: carica due firme → verdetto AUTENTICA / FALSA + score",
        "Tab 3 — Rilevamento Firma: carica documento → immagine annotata con bounding box",
        "Tab 4 — Riconoscimento Entità: inserisci testo → entità evidenziate + tabella",
        "Tab 5 — Identificazione Scrittore: carica campione → candidati ordinati per probabilità",
        "Tab 6 — Analisi Grafologica: carica testo manoscritto → dashboard di metriche",
        "Tutta l'interfaccia è in italiano",
    ], speaker_note="""
La demo interattiva è l'interfaccia pensata per chi non vuole usare Python o Jupyter direttamente. Si apre nel browser, è completamente in italiano, e permette di caricare immagini reali e vedere i risultati in tempo reale. Perfetta per mostrare le funzionalità a clienti, colleghi o stakeholder non tecnici. Avviamola ora per la dimostrazione live — http://localhost:7860.
""")

    add_bullet_with_image(prs,
        "Pipeline End-to-End: dal Documento al Referto Forense",
        [
            "1. Conditional DETR individua le firme nel documento → estrae le immagini",
            "2. SigNet confronta le firme con i riferimenti → verdetto autenticità",
            "3. TrOCR trascrive il testo del documento riga per riga",
            "4. WikiNEural NER estrae persone, luoghi, organizzazioni",
            "5. Analisi grafologica: inclinazione, pressione, spaziatura",
            "Output: referto con score autenticità, entità nominate, metriche grafologiche",
        ],
        imgs.get("pipeline_e2e"),
        note="La pipeline è modulare: ogni lab può essere usato autonomamente o in combinazione.",
        speaker_note="""
Questo diagramma mostra come tutti i pezzi si connettono in una pipeline end-to-end. Un documento entra nel sistema: Conditional DETR trova le firme, SigNet le verifica, TrOCR trascrive il testo, NER estrae le entità, l'analisi grafologica misura le caratteristiche. L'output finale è un referto strutturato con dati quantitativi per ogni dimensione di analisi. La pipeline è modulare: ogni componente può essere usato autonomamente o in combinazione a seconda del tipo di caso.
""")

    add_bullet(prs, "Come Avviare GraphoLab", [
        "Modalità locale (Python 3.11):",
        "  py -3.11 -m venv venv && venv\\Scripts\\activate",
        "  pip install -r requirements.txt",
        "  jupyter lab notebooks/     →  http://localhost:8888",
        "  python app/grapholab_demo.py  →  http://localhost:7860",
        "Modalità Docker (GPU supportata via WSL2):",
        "  docker compose up jupyter   →  http://localhost:8888 (token: grapholab)",
        "  docker compose up gradio    →  http://localhost:7860",
        "Cache modelli nel volume Docker grapholab-hf-cache (download unico)",
    ], font_size=17, speaker_note="""
Per chi vuole provare in locale: serve Python 3.11, poi quattro comandi e siete operativi. Per chi preferisce Docker: due comandi e tutto funziona in un ambiente isolato, senza toccare il sistema. I modelli vengono scaricati automaticamente la prima volta — Hugging Face li mette in cache nel volume Docker, quindi i download successivi sono istantanei. Nessun token HF necessario.
""")

    # =========================================================
    # SEZIONE 5 — Considerazioni Etiche e Conclusioni
    # =========================================================
    add_section(prs, "05", "Considerazioni Etiche\ne Conclusioni",
                "L'AI al servizio della giustizia: opportunità e responsabilità",
                speaker_note="""
Chiudiamo con le considerazioni etiche e legali. Questo è un argomento che i professionisti forensi conoscono bene: la tecnologia è neutrale, ma l'uso che se ne fa non lo è. Alcune riflessioni fondamentali prima di concludere.
""")

    add_bullet(prs, "Bias nei Dati e Spiegabilità", [
        "Bias nei dati di addestramento:",
        "  I modelli riflettono i dati su cui sono stati addestrati",
        "  Soluzione: validare il modello sul tipo specifico di documenti del caso",
        "Spiegabilità in tribunale:",
        "  Le conclusioni peritali devono essere giustificabili e aperte alla contestazione",
        "  La soglia decisionale deve essere documentata e motivata",
        "  L'AI va usata in modo spiegabile in termini non tecnici",
    ], speaker_note="""
Due punti critici. Il bias: un modello addestrato solo su firme europee di adulti sani in condizioni normali potrebbe non funzionare bene su firme di anziani, di persone con condizioni neurologiche, o di culture grafiche diverse. La validazione sul tipo specifico di documenti del caso è quindi fondamentale prima dell'uso forense. La spiegabilità: un giudice deve poter capire come il sistema ha prodotto la sua conclusione. 'Il modello di deep learning ha detto così' non è una risposta accettabile. L'esaminatore deve saper spiegare la metodologia in termini comprensibili.
""")

    add_bullet(prs, "Catena di Custodia e Standard di Riferimento", [
        "Catena di custodia digitale:",
        "  I file originali devono essere conservati inalterati (hash crittografico)",
        "  Ogni passo dell'analisi AI deve essere documentato: versione modello, parametri, data/ora",
        "Standard internazionali di riferimento:",
        "  OSAC — Organization of Scientific Area Committees for Forensic Science",
        "  SWGDOC — Scientific Working Group for Questioned Documents",
        "  Entrambi stanno sviluppando linee guida per gli strumenti computazionali nell'esame dei documenti",
    ], speaker_note="""
La catena di custodia digitale è fondamentale e spesso sottovalutata. I file originali devono essere conservati con hash crittografico — SHA-256 o simili — per dimostrare che non sono stati alterati tra l'acquisizione e l'analisi. Ogni passo dell'analisi AI deve essere documentato: quale versione del modello, quali parametri, quando è stata eseguita. OSAC e SWGDOC sono gli organismi di riferimento internazionale per gli standard forensi e stanno sviluppando linee guida specifiche per gli strumenti computazionali nell'esame dei documenti questionati.
""")

    add_bullet(prs, "AI come Strumento, Non come Sostituto", [
        "L'AI non sostituisce il perito calligrafo — lo potenzia",
        "Cosa rimane esclusivamente umano:",
        "  L'interpretazione dei risultati nel contesto del caso specifico",
        "  La conoscenza specializzata delle dinamiche della scrittura",
        "  Il giudizio professionale e la responsabilità legale del referto",
        "L'AI comprime in minuti ciò che richiederebbe giorni, trasformando impressioni visive in misurazioni precise",
        "Il risultato è un'analisi più rigorosa, più difendibile e più utile alla ricerca della verità",
    ], speaker_note="""
Il messaggio conclusivo e più importante: l'AI è uno strumento nelle mani del professionista. Il referto forense è firmato dall'esperto, non dal computer. La responsabilità legale è dell'esaminatore, non dell'algoritmo. L'AI comprime in minuti ciò che richiederebbe giorni — ma l'interpretazione, il giudizio contestuale, la responsabilità rimangono esclusivamente umani. L'obiettivo di GraphoLab è fornire questi strumenti in modo accessibile, documentato e utilizzabile nella pratica professionale quotidiana.
""")

    # Final slide
    slide = _blank_slide(prs)
    _fill_bg(slide, NAVY)
    _add_rect(slide, 0, SLIDE_H - Inches(0.55), SLIDE_W, Inches(0.55), GOLD)
    _add_textbox(slide, "Grazie per l'attenzione",
                 Inches(1), Inches(1.5), Inches(11.3), Inches(1.5),
                 font_name=TITLE_FONT, font_size=40, color=WHITE, bold=True,
                 align=PP_ALIGN.CENTER)
    _add_textbox(slide, "Domande e discussione",
                 Inches(1), Inches(3.2), Inches(11.3), Inches(0.8),
                 font_size=26, color=GOLD, align=PP_ALIGN.CENTER)
    _add_textbox(slide, "GraphoLab — Intelligenza Artificiale in Grafologia Forense",
                 Inches(1), Inches(4.5), Inches(11.3), Inches(0.6),
                 font_size=16, color=RGBColor(0xAA, 0xBB, 0xCC),
                 align=PP_ALIGN.CENTER)
    _set_speaker_note(slide, """
Grazie per l'attenzione. Sono a disposizione per qualsiasi domanda — sia sui contenuti tecnici che sulle applicazioni pratiche nel vostro lavoro quotidiano. Se volete sperimentare con i vostri dati, i lab sono disponibili e possiamo pianificare una sessione pratica. Il repository GraphoLab è pubblico su GitHub — trovate il link nella slide.
""")


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    print("Generazione diagrammi matplotlib...")
    imgs = generate_diagrams()

    print(f"\nCreazione presentazione ({len(imgs)} figure disponibili)...")
    prs = Presentation()
    prs.slide_width  = SLIDE_W
    prs.slide_height = SLIDE_H

    build(prs, imgs)

    OUTPUT.parent.mkdir(parents=True, exist_ok=True)
    prs.save(OUTPUT)
    print(f"\nPresentazione salvata: {OUTPUT}")
    print(f"Numero di slide: {len(prs.slides)}")


if __name__ == "__main__":
    main()