@piecemaker-legal/piecemaker 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{abnfDiagram-VCTEODGH-DF7LFtT-.js → abnfDiagram-VCTEODGH-CR3_ADBu.js} +1 -1
- package/dist/assets/{arc-xOmjWeVH.js → arc-b92imMmc.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-CESzNPwa.js → architectureDiagram-5GKGNRK7-Dfm5Epud.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-BjdN78Ad.js → blockDiagram-I7D4REHJ-B8kLLyAv.js} +1 -1
- package/dist/assets/{c4Diagram-7LVT6UL2-BcK9EDWT.js → c4Diagram-7LVT6UL2-BWSyAAWX.js} +1 -1
- package/dist/assets/channel-BbcW_Tkh.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-DGUN1JFP.js → chunk-2Q5K7J3B-Ci743mcC.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BG3BBcFr.js → chunk-5VM5RSS4-SdXpal6h.js} +1 -1
- package/dist/assets/{chunk-F27PBJKO-CLIOfrF5.js → chunk-F27PBJKO-Rof94ueX.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-D6lGkqij.js → chunk-IMKFNOWR-B3st6ECc.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BmF9vLBT.js → chunk-JWPE2WC7-alXmu5xU.js} +1 -1
- package/dist/assets/{chunk-POPQ4Y6H-C_0SR3ak.js → chunk-POPQ4Y6H-dMs1OCce.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-rT20Wfpp.js → chunk-SVP7TREG-BmM8FsmF.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-Cn2hkmcf.js → chunk-TICWLB2K-CL1LytYG.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Dm_eP1_v.js → chunk-XXDRQBXY-7U9iRPT1.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-D9_YRZ7i.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-D9_YRZ7i.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-cJeeJsep.js → cose-bilkent-JH36ORCC-DFOMlhj2.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-DX2ePIXe.js → cynefin-OW5HDTMX-CDdJFd9d.js} +1 -1
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-BvWXN-RR.js → cynefinDiagram-5FMLGOSQ-DPyVNrMW.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-CkwPRhGR.js → dagre-GXQ25YYZ-B3bpHa9D.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-CBGNzYyv.js → diagram-S7CK7UJ4-DeFO2rXB.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-CiwCU0SJ.js → diagram-UQ7AKVKN-CbdGTOVf.js} +1 -1
- package/dist/assets/{diagram-VSXAHHWV-Aekg4nTk.js → diagram-VSXAHHWV-DFfNQyMd.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-DPFP5K7r.js → diagram-VX7I27RA-DbNZ6_X3.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-BHJkxoey.js → diagram-Z3DM3KII-B8F-ZkNJ.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-2rV1P32T.js → ebnfDiagram-PWID7BFC-BfCCPPAP.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-C4BjyS_H.js → erDiagram-RLTQ6QDP-DcV-3Kzv.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-DL1TBlAG.js → flowDiagram-HODETNUW-BwSkJXCo.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-MKjvzCWz.js → ganttDiagram-EL5Y4UJY-BffHk77T.js} +1 -1
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-CS_oTGK5.js → gitGraphDiagram-WWUBYQGX-CQ1EWa74.js} +1 -1
- package/dist/assets/{index-XOGQDqU3.js → index-BqK33gaZ.js} +120 -120
- package/dist/assets/index-D_Dc_nQv.css +1 -0
- package/dist/assets/{infoDiagram-27XIBGKW-ihUr2GjN.js → infoDiagram-27XIBGKW-DqIaBXQn.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-D2YAnuir.js → ishikawaDiagram-5VMMS53U-HlhsaZgv.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-Bc2hSZGD.js → journeyDiagram-3NMN7TZE-BYO8-7rN.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-LRMGNonX.js → kanban-definition-UXKFOSKX-DXb8zHGT.js} +1 -1
- package/dist/assets/{layout-CcfWOlPV.js → layout-CREOVuEZ.js} +1 -1
- package/dist/assets/{linear-7JoS2gQO.js → linear-CVCrIRIY.js} +1 -1
- package/dist/assets/{mermaid.core-ijh_SsuN.js → mermaid.core-DFqeWtQR.js} +6 -6
- package/dist/assets/{mindmap-definition-YA3MSWOX-DCE4nzfF.js → mindmap-definition-YA3MSWOX-SUpXz333.js} +1 -1
- package/dist/assets/{pegDiagram-XKGWAZYB-SjaGa7lE.js → pegDiagram-XKGWAZYB-D-yWpve3.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-1xcRidSZ.js → pieDiagram-E7YTZNPT-BJX4Zp4Z.js} +1 -1
- package/dist/assets/{quadrantDiagram-AXDQQJYC-obHAHnYV.js → quadrantDiagram-AXDQQJYC-CHajheTu.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-BsCVcRZb.js → railroadDiagram-O6MQD6OU-DHJWzW5A.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-CUNkhvqb.js → requirementDiagram-BXWQKSXE-DigUIdso.js} +1 -1
- package/dist/assets/{sankeyDiagram-P5KCCOFB-DhezXMKm.js → sankeyDiagram-P5KCCOFB-CFUDDbA5.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-CL9VswXH.js → sequenceDiagram-WJ2MYXX4-6tSaXfYD.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-t0g9jiEz.js → sizeCapture-INFHLROL-CXASEDKe.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH-BW-g-IOz.js → stateDiagram-D77RDMKH-DWTCAQfZ.js} +1 -1
- package/dist/assets/stateDiagram-v2-MP3YSRHH-HDQF4MGA.js +1 -0
- package/dist/assets/{swimlanes-42K2YHIH-LsuyeBte.js → swimlanes-42K2YHIH-7-oz5r-e.js} +1 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-CJbgQFt8.js +8 -0
- package/dist/assets/{timeline-definition-24CTP7MA-B0cFka1R.js → timeline-definition-24CTP7MA-DVH5g4ty.js} +1 -1
- package/dist/assets/{vendor-codemirror-C5qqu4R7.js → vendor-codemirror-CjW0lHR5.js} +5 -5
- package/dist/assets/{vennDiagram-4TSXK5OY-D3HNCDR3.js → vennDiagram-4TSXK5OY-CQvY_ALZ.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-DJ3Co8nb.js → wardleyDiagram-VM6X3IG4-BdlTJgEc.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-Cz5E9New.js → xychartDiagram-S5SC5T6Z-CYn2sV77.js} +1 -1
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +1 -9
- package/dist/generate-icons.js +47 -46
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-128x128.svg +1 -12
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-144x144.svg +1 -12
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-152x152.svg +1 -12
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-192x192.svg +1 -12
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-384x384.svg +1 -12
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-512x512.svg +1 -12
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-72x72.svg +1 -12
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/icons/icon-96x96.svg +1 -12
- package/dist/index.html +3 -3
- package/dist/logo-128.png +0 -0
- package/dist/logo-256.png +0 -0
- package/dist/logo-32.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-64.png +0 -0
- package/dist/logo-sources/logo-black.png +0 -0
- package/dist/logo-sources/logo-white.png +0 -0
- package/dist/logo.svg +1 -17
- package/dist/sw.js +1 -1
- package/dist-server/plugins/piecemaker-dossier/src/institutional-terms.js +91 -0
- package/dist-server/plugins/piecemaker-dossier/src/institutional-terms.js.map +1 -0
- package/dist-server/plugins/piecemaker-dossier/src/knowledge.js +112 -10
- package/dist-server/plugins/piecemaker-dossier/src/knowledge.js.map +1 -1
- package/dist-server/plugins/piecemaker-dossier/src/scan-result.js +5 -1
- package/dist-server/plugins/piecemaker-dossier/src/scan-result.js.map +1 -1
- package/dist-server/server/index.js +60 -26
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/database/repositories/projects.db.js +28 -6
- package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -1
- package/dist-server/server/modules/database/tests/projects.db.integration.test.js +58 -1
- package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/plugins/plugins.routes.js +8 -1
- package/dist-server/server/modules/plugins/plugins.routes.js.map +1 -1
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +2 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
- package/dist-server/server/piecemaker/bodacc-search.js +191 -0
- package/dist-server/server/piecemaker/bodacc-search.js.map +1 -0
- package/dist-server/server/piecemaker/company-search.js +145 -0
- package/dist-server/server/piecemaker/company-search.js.map +1 -0
- package/dist-server/server/piecemaker/company-search.test.js +18 -0
- package/dist-server/server/piecemaker/company-search.test.js.map +1 -0
- package/dist-server/server/piecemaker/index.js +6 -2
- package/dist-server/server/piecemaker/index.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/index.js +9 -4
- package/dist-server/server/piecemaker/knowledge/index.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/pipeline.js +68 -58
- package/dist-server/server/piecemaker/knowledge/pipeline.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/routes.js +2 -4
- package/dist-server/server/piecemaker/knowledge/routes.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/scan-jobs.js +91 -0
- package/dist-server/server/piecemaker/knowledge/scan-jobs.js.map +1 -0
- package/dist-server/server/piecemaker/knowledge/service.js +18 -36
- package/dist-server/server/piecemaker/knowledge/service.js.map +1 -1
- package/dist-server/server/piecemaker/library/connector-installation.js +224 -0
- package/dist-server/server/piecemaker/library/connector-installation.js.map +1 -0
- package/dist-server/server/piecemaker/library/index.js +11 -1
- package/dist-server/server/piecemaker/library/index.js.map +1 -1
- package/dist-server/server/piecemaker/library/marketplace.js +102 -43
- package/dist-server/server/piecemaker/library/marketplace.js.map +1 -1
- package/dist-server/server/piecemaker/library/migrate.js +8 -1
- package/dist-server/server/piecemaker/library/migrate.js.map +1 -1
- package/dist-server/server/piecemaker/library/provider-agents.js +50 -0
- package/dist-server/server/piecemaker/library/provider-agents.js.map +1 -0
- package/dist-server/server/piecemaker/library/provider-connectors.js +80 -0
- package/dist-server/server/piecemaker/library/provider-connectors.js.map +1 -0
- package/dist-server/server/piecemaker/library/provider-skills.js +11 -2
- package/dist-server/server/piecemaker/library/provider-skills.js.map +1 -1
- package/dist-server/server/piecemaker/library/store.js +125 -35
- package/dist-server/server/piecemaker/library/store.js.map +1 -1
- package/dist-server/server/piecemaker/library/workspace-installation.js +53 -29
- package/dist-server/server/piecemaker/library/workspace-installation.js.map +1 -1
- package/dist-server/server/piecemaker/timesheet/service.js +4 -21
- package/dist-server/server/piecemaker/timesheet/service.js.map +1 -1
- package/package.json +3 -2
- package/scripts/piecemaker/cli/install-command.mjs +35 -9
- package/scripts/piecemaker/cli/lib/config.mjs +1 -0
- package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +78 -0
- package/scripts/piecemaker/cli/lib/plugins.mjs +2 -0
- package/scripts/piecemaker/cli/lib/pwa.mjs +46 -7
- package/scripts/piecemaker/cli/lib/services.mjs +65 -4
- package/scripts/piecemaker/cli/piecemaker-command.test.mjs +25 -0
- package/scripts/piecemaker/cli/piecemaker.mjs +45 -44
- package/scripts/piecemaker/cli/piecemaker.ps1 +8 -0
- package/scripts/piecemaker/cli/piecemaker.sh +6 -1
- package/server/index.ts +52 -23
- package/server/modules/database/repositories/projects.db.ts +28 -6
- package/server/modules/database/tests/projects.db.integration.test.ts +63 -1
- package/server/modules/plugins/plugins.routes.ts +8 -1
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +5 -0
- package/server/piecemaker/anonymizer/client-config.cjs +10 -2
- package/server/piecemaker/anonymizer/dictionary.cjs +6 -72
- package/server/piecemaker/bodacc-search.ts +205 -0
- package/server/piecemaker/company-search.test.ts +26 -0
- package/server/piecemaker/company-search.ts +183 -0
- package/server/piecemaker/index.ts +8 -2
- package/server/piecemaker/knowledge/index.ts +9 -4
- package/server/piecemaker/knowledge/pipeline.ts +89 -57
- package/server/piecemaker/knowledge/routes.ts +2 -4
- package/server/piecemaker/knowledge/scan-jobs.ts +111 -0
- package/server/piecemaker/knowledge/service.ts +18 -33
- package/server/piecemaker/library/connector-installation.ts +248 -0
- package/server/piecemaker/library/index.ts +5 -1
- package/server/piecemaker/library/marketplace.ts +104 -39
- package/server/piecemaker/library/migrate.ts +8 -1
- package/server/piecemaker/library/provider-agents.ts +49 -0
- package/server/piecemaker/library/provider-connectors.ts +89 -0
- package/server/piecemaker/library/provider-skills.ts +10 -1
- package/server/piecemaker/library/store.ts +129 -36
- package/server/piecemaker/library/workspace-installation.ts +54 -29
- package/server/piecemaker/router.cjs +2 -1
- package/server/piecemaker/timesheet/service.ts +4 -16
- package/server/piecemaker/vendor/installer/steps/03-python-gliner.mjs +0 -27
- package/server/piecemaker/vendor/installer/steps/14-mxc-sandbox.mjs +1 -1
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/substitution.cjs +2 -3
- package/server/piecemaker/vendor/piecemaker-plugin/skills/conversion-md/SKILL.md +3 -3
- package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +3 -10
- package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +135 -4
- package/server/piecemaker/vendor/websocket-server/process-group.cjs +95 -0
- package/server/piecemaker/vendor/websocket-server/scripts/__pycache__/scan_utils.cpython-312.pyc +0 -0
- package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +280 -149
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/__pycache__/model_config.cpython-312.pyc +0 -0
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/__pycache__/scanner_worker.cpython-312.pyc +0 -0
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/scanner_worker.py +35 -29
- package/server/piecemaker/vendor/websocket-server/scripts/requirements.txt +0 -5
- package/server/shared/types.ts +1 -0
- package/dist/assets/channel-Dz3eE5vU.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-BC1eKV2j.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-BC1eKV2j.js +0 -1
- package/dist/assets/index-DhwJpHCq.css +0 -1
- package/dist/assets/stateDiagram-v2-MP3YSRHH-BXuT4zDG.js +0 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-B0eYrB4U.js +0 -8
- package/server/piecemaker/vendor/websocket-server/case-instructions.cjs +0 -111
- package/server/piecemaker/vendor/websocket-server/scripts/ADDRESS_ANONYMIZATION.md +0 -216
- package/server/piecemaker/vendor/websocket-server/scripts/DEDUPLICATION_CHANGES.md +0 -134
- package/server/piecemaker/vendor/websocket-server/scripts/GLINER2_README.md +0 -138
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/AUDIT_PERF_QUALITE.md +0 -246
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/MODELE_COREML.md +0 -62
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/__pycache__/coreml_runtime.cpython-312.pyc +0 -0
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/build_coreml.py +0 -118
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/coreml_runtime.py +0 -164
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/presidio-gliner.py +0 -479
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Convert & Scan Pipeline — orchestrates document conversion and PII scanning.
|
|
4
4
|
|
|
5
5
|
This script chains smart_converter.py (conversion to Markdown) and
|
|
6
|
-
|
|
6
|
+
scanner_worker.py (PII scanning) for batch processing of multiple documents.
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
9
|
python3 convert_and_scan_pipeline.py <file1> [file2 ...] -o <output_dir> [options]
|
|
@@ -48,6 +48,10 @@ from pathlib import Path
|
|
|
48
48
|
from typing import List, Optional, Tuple, Dict, Set
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
GLINER_CHUNK_SIZE = 384
|
|
52
|
+
GLINER_CHUNK_OVERLAP = 64
|
|
53
|
+
|
|
54
|
+
|
|
51
55
|
def print_progress(phase: str, current: int, total: int) -> None:
|
|
52
56
|
"""Print standardized progress message.
|
|
53
57
|
|
|
@@ -93,7 +97,11 @@ def start_scanner_worker() -> Optional[subprocess.Popen]:
|
|
|
93
97
|
return None
|
|
94
98
|
|
|
95
99
|
|
|
96
|
-
def wait_for_worker_ready(
|
|
100
|
+
def wait_for_worker_ready(
|
|
101
|
+
proc: Optional[subprocess.Popen],
|
|
102
|
+
progress_context: Dict,
|
|
103
|
+
timeout: int = 500,
|
|
104
|
+
) -> bool:
|
|
97
105
|
"""Wait for the scanner worker to print READY on stdout.
|
|
98
106
|
|
|
99
107
|
While waiting, streams stderr output (model loading progress) in real-time.
|
|
@@ -108,9 +116,9 @@ def wait_for_worker_ready(proc: Optional[subprocess.Popen], timeout: int = 500)
|
|
|
108
116
|
if proc is None:
|
|
109
117
|
return False
|
|
110
118
|
|
|
111
|
-
import re
|
|
112
119
|
import select
|
|
113
120
|
import threading
|
|
121
|
+
progress_context["event"] = threading.Event()
|
|
114
122
|
|
|
115
123
|
_chunk_re = re.compile(r"^PROGRESS:CHUNKS:\d+:\d+:\d+")
|
|
116
124
|
|
|
@@ -126,9 +134,24 @@ def wait_for_worker_ready(proc: Optional[subprocess.Popen], timeout: int = 500)
|
|
|
126
134
|
for line in proc.stderr:
|
|
127
135
|
stripped = line.strip()
|
|
128
136
|
if _chunk_re.match(stripped):
|
|
129
|
-
|
|
137
|
+
_, _, _, current, raw_total = stripped.split(":")
|
|
138
|
+
global_total = progress_context.get("total", 0)
|
|
139
|
+
if global_total:
|
|
140
|
+
global_current = min(global_total, progress_context.get("offset", 0) + int(current))
|
|
141
|
+
global_percent = int(global_current * 100 / global_total)
|
|
142
|
+
print(
|
|
143
|
+
f"PROGRESS:CHUNKS:{global_percent}:{global_current}:{global_total}",
|
|
144
|
+
flush=True,
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
print(stripped, flush=True)
|
|
148
|
+
if int(current) >= int(raw_total):
|
|
149
|
+
progress_context["event"].set()
|
|
130
150
|
else:
|
|
131
|
-
|
|
151
|
+
# Keep diagnostics on stderr. The Node parent deliberately
|
|
152
|
+
# ignores non-PROGRESS stdout because it may contain document
|
|
153
|
+
# text, while it retains a bounded stderr tail on failure.
|
|
154
|
+
print(f" [worker] {line}", end="", file=sys.stderr, flush=True)
|
|
132
155
|
except (ValueError, OSError):
|
|
133
156
|
pass # Pipe closed
|
|
134
157
|
|
|
@@ -141,11 +164,12 @@ def wait_for_worker_ready(proc: Optional[subprocess.Popen], timeout: int = 500)
|
|
|
141
164
|
print(f"⚠️ Scanner worker exited prematurely (exit {proc.returncode})", file=sys.stderr)
|
|
142
165
|
return False
|
|
143
166
|
|
|
144
|
-
#
|
|
167
|
+
# TextIOWrapper does not reliably distinguish EAGAIN from EOF when its
|
|
168
|
+
# descriptor is toggled to non-blocking mode. select() keeps the stream
|
|
169
|
+
# blocking and reads only after a complete line is available.
|
|
145
170
|
try:
|
|
146
|
-
|
|
147
|
-
line = proc.stdout.readline()
|
|
148
|
-
os.set_blocking(proc.stdout.fileno(), True)
|
|
171
|
+
readable, _, _ = select.select([proc.stdout], [], [], 0.1)
|
|
172
|
+
line = proc.stdout.readline() if readable else ""
|
|
149
173
|
except (IOError, OSError):
|
|
150
174
|
line = ""
|
|
151
175
|
|
|
@@ -153,16 +177,23 @@ def wait_for_worker_ready(proc: Optional[subprocess.Popen], timeout: int = 500)
|
|
|
153
177
|
print("✅ Scanner worker ready (models loaded)")
|
|
154
178
|
return True
|
|
155
179
|
|
|
156
|
-
if not line:
|
|
157
|
-
time.sleep(0.1)
|
|
158
|
-
|
|
159
180
|
# Timeout
|
|
160
181
|
print("⚠️ Scanner worker timed out waiting for READY", file=sys.stderr)
|
|
161
182
|
proc.kill()
|
|
183
|
+
try:
|
|
184
|
+
proc.wait(timeout=5)
|
|
185
|
+
except subprocess.TimeoutExpired:
|
|
186
|
+
pass
|
|
162
187
|
return False
|
|
163
188
|
|
|
164
189
|
|
|
165
|
-
def scan_file_via_worker(
|
|
190
|
+
def scan_file_via_worker(
|
|
191
|
+
proc: subprocess.Popen,
|
|
192
|
+
md_file: str,
|
|
193
|
+
output_dir: str,
|
|
194
|
+
progress_context: Optional[Dict] = None,
|
|
195
|
+
chunk_offset: int = 0,
|
|
196
|
+
) -> bool:
|
|
166
197
|
"""Send a scan command to the worker and wait for the result.
|
|
167
198
|
|
|
168
199
|
Args:
|
|
@@ -173,6 +204,9 @@ def scan_file_via_worker(proc: subprocess.Popen, md_file: str, output_dir: str)
|
|
|
173
204
|
Returns:
|
|
174
205
|
True if scan succeeded, False otherwise.
|
|
175
206
|
"""
|
|
207
|
+
if progress_context is not None:
|
|
208
|
+
progress_context["offset"] = chunk_offset
|
|
209
|
+
progress_context["event"].clear()
|
|
176
210
|
cmd = json.dumps({"cmd": "scan", "md_file": md_file, "output_dir": output_dir})
|
|
177
211
|
try:
|
|
178
212
|
proc.stdin.write(cmd + "\n")
|
|
@@ -198,6 +232,9 @@ def scan_file_via_worker(proc: subprocess.Popen, md_file: str, output_dir: str)
|
|
|
198
232
|
print(f"⚠️ Invalid JSON from worker: {response_line.strip()}", file=sys.stderr)
|
|
199
233
|
return False
|
|
200
234
|
|
|
235
|
+
if progress_context is not None and progress_context.get("active_chunks", 0):
|
|
236
|
+
progress_context["event"].wait(timeout=5)
|
|
237
|
+
|
|
201
238
|
if response.get("status") == "ok":
|
|
202
239
|
return True
|
|
203
240
|
else:
|
|
@@ -207,15 +244,43 @@ def scan_file_via_worker(proc: subprocess.Popen, md_file: str, output_dir: str)
|
|
|
207
244
|
|
|
208
245
|
def stop_scanner_worker(proc: Optional[subprocess.Popen]) -> None:
|
|
209
246
|
"""Gracefully stop the scanner worker."""
|
|
210
|
-
if proc is None
|
|
247
|
+
if proc is None:
|
|
211
248
|
return
|
|
212
249
|
|
|
213
250
|
try:
|
|
214
|
-
proc.
|
|
215
|
-
|
|
216
|
-
|
|
251
|
+
if proc.poll() is None:
|
|
252
|
+
proc.stdin.write(json.dumps({"cmd": "quit"}) + "\n")
|
|
253
|
+
proc.stdin.flush()
|
|
254
|
+
proc.wait(timeout=10)
|
|
217
255
|
except Exception:
|
|
218
|
-
|
|
256
|
+
try:
|
|
257
|
+
proc.kill()
|
|
258
|
+
proc.wait(timeout=5)
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
finally:
|
|
262
|
+
for stream in (proc.stdin, proc.stdout, proc.stderr):
|
|
263
|
+
try:
|
|
264
|
+
stream.close()
|
|
265
|
+
except (AttributeError, OSError):
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class PipelineResources:
|
|
270
|
+
"""Owns every heavyweight or sensitive transient pipeline resource."""
|
|
271
|
+
|
|
272
|
+
def __init__(self) -> None:
|
|
273
|
+
self.scanner_worker: Optional[subprocess.Popen] = None
|
|
274
|
+
self.scan_workspace: Optional[tempfile.TemporaryDirectory] = None
|
|
275
|
+
|
|
276
|
+
def close(self) -> None:
|
|
277
|
+
# The temporary maps contain raw PII and must be removed even when
|
|
278
|
+
# worker shutdown itself encounters a broken pipe or a dead process.
|
|
279
|
+
try:
|
|
280
|
+
stop_scanner_worker(self.scanner_worker)
|
|
281
|
+
finally:
|
|
282
|
+
if self.scan_workspace is not None:
|
|
283
|
+
self.scan_workspace.cleanup()
|
|
219
284
|
|
|
220
285
|
|
|
221
286
|
def convert_file(
|
|
@@ -328,100 +393,6 @@ def convert_file(
|
|
|
328
393
|
return False, None
|
|
329
394
|
|
|
330
395
|
|
|
331
|
-
def scan_file(md_file: str, output_dir: str) -> bool:
|
|
332
|
-
"""Run presidio-gliner.py subprocess to scan Markdown for PII with real-time output streaming.
|
|
333
|
-
|
|
334
|
-
Args:
|
|
335
|
-
md_file: Path to markdown file
|
|
336
|
-
output_dir: Directory for output JSON
|
|
337
|
-
|
|
338
|
-
Returns:
|
|
339
|
-
success: True if scan succeeded
|
|
340
|
-
"""
|
|
341
|
-
script_dir = Path(__file__).parent
|
|
342
|
-
scanner_script = script_dir / "presidio-gliner" / "presidio-gliner.py"
|
|
343
|
-
|
|
344
|
-
if not scanner_script.exists():
|
|
345
|
-
print(f"❌ presidio-gliner.py not found at {scanner_script}", file=sys.stderr)
|
|
346
|
-
return False
|
|
347
|
-
|
|
348
|
-
# Build command
|
|
349
|
-
cmd = [sys.executable, str(scanner_script), md_file, "-o", output_dir]
|
|
350
|
-
|
|
351
|
-
try:
|
|
352
|
-
# Run scanner with real-time output streaming
|
|
353
|
-
process = subprocess.Popen(
|
|
354
|
-
cmd,
|
|
355
|
-
stdout=subprocess.PIPE,
|
|
356
|
-
stderr=subprocess.PIPE,
|
|
357
|
-
text=True,
|
|
358
|
-
bufsize=1, # Line buffered
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
# Stream output in real-time
|
|
362
|
-
os.set_blocking(process.stdout.fileno(), False)
|
|
363
|
-
os.set_blocking(process.stderr.fileno(), False)
|
|
364
|
-
|
|
365
|
-
return_code = None
|
|
366
|
-
while return_code is None:
|
|
367
|
-
# Check if process has finished
|
|
368
|
-
return_code = process.poll()
|
|
369
|
-
|
|
370
|
-
# Read available output without blocking
|
|
371
|
-
try:
|
|
372
|
-
# Read stdout
|
|
373
|
-
while True:
|
|
374
|
-
line = process.stdout.readline()
|
|
375
|
-
if not line:
|
|
376
|
-
break
|
|
377
|
-
print(line, end="", flush=True)
|
|
378
|
-
|
|
379
|
-
# Read stderr
|
|
380
|
-
while True:
|
|
381
|
-
line = process.stderr.readline()
|
|
382
|
-
if not line:
|
|
383
|
-
break
|
|
384
|
-
print(line, end="", file=sys.stderr, flush=True)
|
|
385
|
-
|
|
386
|
-
except (IOError, OSError):
|
|
387
|
-
pass
|
|
388
|
-
|
|
389
|
-
# Small delay to prevent CPU spinning
|
|
390
|
-
if return_code is None:
|
|
391
|
-
time.sleep(0.01)
|
|
392
|
-
|
|
393
|
-
# Ensure we read any remaining output
|
|
394
|
-
try:
|
|
395
|
-
remaining_stdout, remaining_stderr = process.communicate(timeout=1)
|
|
396
|
-
if remaining_stdout:
|
|
397
|
-
print(remaining_stdout, end="", flush=True)
|
|
398
|
-
if remaining_stderr:
|
|
399
|
-
print(remaining_stderr, end="", file=sys.stderr, flush=True)
|
|
400
|
-
except subprocess.TimeoutExpired:
|
|
401
|
-
pass
|
|
402
|
-
|
|
403
|
-
if return_code != 0:
|
|
404
|
-
print(f"⚠️ Scan failed with exit code {return_code}", file=sys.stderr)
|
|
405
|
-
return False
|
|
406
|
-
|
|
407
|
-
# Verify JSON output was created
|
|
408
|
-
md_stem = Path(md_file).stem
|
|
409
|
-
json_path = Path(output_dir) / f"{md_stem}_sensitive_map.json"
|
|
410
|
-
|
|
411
|
-
if not json_path.exists():
|
|
412
|
-
print(f"⚠️ Expected JSON output not found: {json_path}", file=sys.stderr)
|
|
413
|
-
return False
|
|
414
|
-
|
|
415
|
-
return True
|
|
416
|
-
|
|
417
|
-
except subprocess.TimeoutExpired:
|
|
418
|
-
print(f"❌ Scan timeout (>10 minutes): {md_file}", file=sys.stderr)
|
|
419
|
-
return False
|
|
420
|
-
except Exception as e:
|
|
421
|
-
print(f"❌ Scan error: {e}", file=sys.stderr)
|
|
422
|
-
return False
|
|
423
|
-
|
|
424
|
-
|
|
425
396
|
def load_existing_mapping(mapping_path: Path) -> Optional[Dict]:
|
|
426
397
|
"""Load the case/document mapping file if it exists.
|
|
427
398
|
|
|
@@ -568,6 +539,25 @@ def are_names_similar(name1: str, norm1: str, name2: str, norm2: str) -> bool:
|
|
|
568
539
|
return False
|
|
569
540
|
|
|
570
541
|
|
|
542
|
+
def count_gliner_chunks(md_file: str) -> int:
|
|
543
|
+
try:
|
|
544
|
+
word_count = len(re.findall(r"\S+", Path(md_file).read_text(encoding="utf-8")))
|
|
545
|
+
except (OSError, UnicodeError):
|
|
546
|
+
return 1
|
|
547
|
+
if not word_count:
|
|
548
|
+
return 1
|
|
549
|
+
step = max(1, GLINER_CHUNK_SIZE - GLINER_CHUNK_OVERLAP)
|
|
550
|
+
return (max(0, word_count - GLINER_CHUNK_SIZE) + step - 1) // step + 1
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def emit_chunk_progress(current: int, total: int) -> None:
|
|
554
|
+
if total <= 0:
|
|
555
|
+
return
|
|
556
|
+
bounded_current = min(total, max(0, current))
|
|
557
|
+
percent = int(bounded_current * 100 / total)
|
|
558
|
+
print(f"PROGRESS:CHUNKS:{percent}:{bounded_current}:{total}", flush=True)
|
|
559
|
+
|
|
560
|
+
|
|
571
561
|
def consolidate_duplicate_entities(entities: List[Dict]) -> List[Dict]:
|
|
572
562
|
"""Consolidate duplicate entities into single entries with variants.
|
|
573
563
|
|
|
@@ -643,6 +633,84 @@ def consolidate_duplicate_entities(entities: List[Dict]) -> List[Dict]:
|
|
|
643
633
|
return consolidated
|
|
644
634
|
|
|
645
635
|
|
|
636
|
+
def _person_entry_texts(code: str, entry: Dict, reverse_mapping: Dict,
|
|
637
|
+
mapping_texts: Optional[List[str]] = None) -> List[str]:
|
|
638
|
+
values = [entry.get("original", ""), *(entry.get("variants") or [])]
|
|
639
|
+
values.extend(reverse_mapping.get(code, []) or [])
|
|
640
|
+
values.extend(mapping_texts or [])
|
|
641
|
+
return list(dict.fromkeys(
|
|
642
|
+
value for value in values
|
|
643
|
+
if isinstance(value, str) and value.strip()
|
|
644
|
+
))
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _merge_person_entry(merged_mapping: Dict, merged_reverse: Dict,
|
|
648
|
+
merged_extracted: Dict, code: str, texts: List[str],
|
|
649
|
+
source_entry: Optional[Dict] = None) -> None:
|
|
650
|
+
entries = merged_extracted.setdefault("personnes_physiques", {})
|
|
651
|
+
entry = entries.setdefault(code, {
|
|
652
|
+
"original": texts[0] if texts else "",
|
|
653
|
+
"code": code,
|
|
654
|
+
"variants": [],
|
|
655
|
+
})
|
|
656
|
+
current_texts = _person_entry_texts(code, entry, merged_reverse)
|
|
657
|
+
combined = list(dict.fromkeys([*current_texts, *texts]))
|
|
658
|
+
if combined:
|
|
659
|
+
entry["original"] = max(combined, key=len)
|
|
660
|
+
entry["variants"] = combined
|
|
661
|
+
merged_reverse[code] = list(dict.fromkeys([
|
|
662
|
+
*(merged_reverse.get(code, []) or []), *combined
|
|
663
|
+
]))
|
|
664
|
+
for text in combined:
|
|
665
|
+
merged_mapping[text] = code
|
|
666
|
+
if source_entry:
|
|
667
|
+
entry["score"] = max(entry.get("score", 0.0), source_entry.get("score", 0.0))
|
|
668
|
+
entry.setdefault("recognizer", source_entry.get("recognizer", "unknown"))
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _prune_existing_person_duplicates(merged_mapping: Dict, merged_reverse: Dict,
|
|
672
|
+
merged_extracted: Dict) -> None:
|
|
673
|
+
entries = merged_extracted.setdefault("personnes_physiques", {})
|
|
674
|
+
mapping_texts_by_code: Dict[str, List[str]] = {}
|
|
675
|
+
for text, code in merged_mapping.items():
|
|
676
|
+
mapping_texts_by_code.setdefault(code, []).append(text)
|
|
677
|
+
|
|
678
|
+
canonical_by_name: Dict[str, str] = {}
|
|
679
|
+
for code in list(entries):
|
|
680
|
+
entry = entries.get(code, {})
|
|
681
|
+
texts = _person_entry_texts(
|
|
682
|
+
code,
|
|
683
|
+
entry,
|
|
684
|
+
merged_reverse,
|
|
685
|
+
mapping_texts_by_code.get(code),
|
|
686
|
+
)
|
|
687
|
+
names = [normalize_name(text) for text in texts]
|
|
688
|
+
names = list(dict.fromkeys(name for name in names if name))
|
|
689
|
+
survivor = next((canonical_by_name[name] for name in names if name in canonical_by_name), None)
|
|
690
|
+
if survivor is None:
|
|
691
|
+
for name in names:
|
|
692
|
+
canonical_by_name[name] = code
|
|
693
|
+
continue
|
|
694
|
+
if survivor == code:
|
|
695
|
+
continue
|
|
696
|
+
|
|
697
|
+
_merge_person_entry(
|
|
698
|
+
merged_mapping,
|
|
699
|
+
merged_reverse,
|
|
700
|
+
merged_extracted,
|
|
701
|
+
survivor,
|
|
702
|
+
texts,
|
|
703
|
+
entry,
|
|
704
|
+
)
|
|
705
|
+
for name in names:
|
|
706
|
+
canonical_by_name[name] = survivor
|
|
707
|
+
for text, mapped_code in list(merged_mapping.items()):
|
|
708
|
+
if mapped_code == code:
|
|
709
|
+
merged_mapping[text] = survivor
|
|
710
|
+
merged_reverse.pop(code, None)
|
|
711
|
+
entries.pop(code, None)
|
|
712
|
+
|
|
713
|
+
|
|
646
714
|
def is_known_city_or_country(text: str) -> bool:
|
|
647
715
|
"""Check if text matches known city or country names.
|
|
648
716
|
|
|
@@ -1188,8 +1256,21 @@ def merge_with_existing_mapping(new_mapping: Dict, existing_mapping: Optional[Di
|
|
|
1188
1256
|
"autres": {**existing_mapping.get('extracted_data', {}).get('autres', {})}
|
|
1189
1257
|
}
|
|
1190
1258
|
|
|
1191
|
-
|
|
1259
|
+
_prune_existing_person_duplicates(merged_mapping, merged_reverse, merged_extracted)
|
|
1260
|
+
|
|
1192
1261
|
seen_entities_lower = {k.lower(): v for k, v in merged_mapping.items()}
|
|
1262
|
+
existing_person_by_name: Dict[str, str] = {}
|
|
1263
|
+
for code, entry in merged_extracted["personnes_physiques"].items():
|
|
1264
|
+
for entity_text in _person_entry_texts(code, entry, merged_reverse):
|
|
1265
|
+
normalized = normalize_name(entity_text)
|
|
1266
|
+
if normalized:
|
|
1267
|
+
existing_person_by_name.setdefault(normalized, code)
|
|
1268
|
+
for entity_text, code in merged_mapping.items():
|
|
1269
|
+
if "PERSONNE_PHYSIQUE_" not in str(code) and not str(code).startswith(("DIRIGEANT_", "AVOCAT_")):
|
|
1270
|
+
continue
|
|
1271
|
+
normalized = normalize_name(entity_text)
|
|
1272
|
+
if normalized:
|
|
1273
|
+
existing_person_by_name.setdefault(normalized, code)
|
|
1193
1274
|
|
|
1194
1275
|
# Find highest existing code numbers per category
|
|
1195
1276
|
code_counters = {
|
|
@@ -1204,7 +1285,7 @@ def merge_with_existing_mapping(new_mapping: Dict, existing_mapping: Optional[Di
|
|
|
1204
1285
|
# URGOT SA), là où un test par sous-chaîne ne reconnaîtrait pas SA_3.
|
|
1205
1286
|
societe_counters: Dict[str, int] = {}
|
|
1206
1287
|
|
|
1207
|
-
for code in merged_reverse.
|
|
1288
|
+
for code in set(merged_reverse) | set(merged_mapping.values()):
|
|
1208
1289
|
if "PERSONNE_PHYSIQUE_" in code or code.startswith("DIRIGEANT_"):
|
|
1209
1290
|
num = int(code.split("_")[-1])
|
|
1210
1291
|
code_counters["personnes_physiques"] = max(code_counters["personnes_physiques"], num + 1)
|
|
@@ -1226,15 +1307,6 @@ def merge_with_existing_mapping(new_mapping: Dict, existing_mapping: Optional[Di
|
|
|
1226
1307
|
for text, code in new_mapping.get('mapping', {}).items():
|
|
1227
1308
|
text_lower = text.lower()
|
|
1228
1309
|
|
|
1229
|
-
# Skip if already exists (use existing code)
|
|
1230
|
-
if text_lower in seen_entities_lower:
|
|
1231
|
-
continue
|
|
1232
|
-
|
|
1233
|
-
# Skip entities the lawyer discarded from the mapping
|
|
1234
|
-
if text_lower in ignored_lower:
|
|
1235
|
-
continue
|
|
1236
|
-
|
|
1237
|
-
# Determine category
|
|
1238
1310
|
category = None
|
|
1239
1311
|
for cat_key in merged_extracted.keys():
|
|
1240
1312
|
if code in new_mapping.get('extracted_data', {}).get(cat_key, {}):
|
|
@@ -1244,6 +1316,42 @@ def merge_with_existing_mapping(new_mapping: Dict, existing_mapping: Optional[Di
|
|
|
1244
1316
|
if not category:
|
|
1245
1317
|
category = "autres"
|
|
1246
1318
|
|
|
1319
|
+
old_entry = new_mapping.get('extracted_data', {}).get(category, {}).get(code, {})
|
|
1320
|
+
variants = old_entry.get("variants", [text])
|
|
1321
|
+
candidate_texts = list(dict.fromkeys([text, *variants]))
|
|
1322
|
+
|
|
1323
|
+
if category == "personnes_physiques":
|
|
1324
|
+
existing_code = next(
|
|
1325
|
+
(
|
|
1326
|
+
existing_person_by_name[normalized]
|
|
1327
|
+
for candidate in candidate_texts
|
|
1328
|
+
for normalized in [normalize_name(candidate)]
|
|
1329
|
+
if normalized in existing_person_by_name
|
|
1330
|
+
),
|
|
1331
|
+
None,
|
|
1332
|
+
)
|
|
1333
|
+
if existing_code:
|
|
1334
|
+
accepted_texts = [candidate for candidate in candidate_texts if candidate.lower() not in ignored_lower]
|
|
1335
|
+
_merge_person_entry(
|
|
1336
|
+
merged_mapping,
|
|
1337
|
+
merged_reverse,
|
|
1338
|
+
merged_extracted,
|
|
1339
|
+
existing_code,
|
|
1340
|
+
accepted_texts,
|
|
1341
|
+
old_entry,
|
|
1342
|
+
)
|
|
1343
|
+
for candidate in accepted_texts:
|
|
1344
|
+
seen_entities_lower[candidate.lower()] = existing_code
|
|
1345
|
+
continue
|
|
1346
|
+
|
|
1347
|
+
# Skip if already exists (use existing code)
|
|
1348
|
+
if text_lower in seen_entities_lower:
|
|
1349
|
+
continue
|
|
1350
|
+
|
|
1351
|
+
# Skip entities the lawyer discarded from the mapping
|
|
1352
|
+
if text_lower in ignored_lower:
|
|
1353
|
+
continue
|
|
1354
|
+
|
|
1247
1355
|
# Generate new code with incremented counter
|
|
1248
1356
|
if category == "personnes_physiques":
|
|
1249
1357
|
new_code = f"PERSONNE_PHYSIQUE_{code_counters[category]:02d}"
|
|
@@ -1267,13 +1375,14 @@ def merge_with_existing_mapping(new_mapping: Dict, existing_mapping: Optional[Di
|
|
|
1267
1375
|
new_code = f"{entity_type}_{code_counters[category]:02d}"
|
|
1268
1376
|
code_counters[category] += 1
|
|
1269
1377
|
|
|
1270
|
-
# Get variants from existing entry
|
|
1271
|
-
old_entry = new_mapping.get('extracted_data', {}).get(category, {}).get(code, {})
|
|
1272
|
-
variants = old_entry.get("variants", [text])
|
|
1273
|
-
|
|
1274
1378
|
# Add all variants to merged structures (no lowercase duplicates)
|
|
1275
|
-
for variant in variants
|
|
1379
|
+
accepted_variants = [variant for variant in variants if variant.lower() not in ignored_lower]
|
|
1380
|
+
for variant in accepted_variants:
|
|
1276
1381
|
merged_mapping[variant] = new_code
|
|
1382
|
+
seen_entities_lower[variant.lower()] = new_code
|
|
1383
|
+
normalized = normalize_name(variant) if category == "personnes_physiques" else ""
|
|
1384
|
+
if normalized:
|
|
1385
|
+
existing_person_by_name.setdefault(normalized, new_code)
|
|
1277
1386
|
|
|
1278
1387
|
seen_entities_lower[text_lower] = new_code
|
|
1279
1388
|
|
|
@@ -1285,7 +1394,7 @@ def merge_with_existing_mapping(new_mapping: Dict, existing_mapping: Optional[Di
|
|
|
1285
1394
|
merged_extracted[category][new_code] = {
|
|
1286
1395
|
"original": original,
|
|
1287
1396
|
"code": new_code,
|
|
1288
|
-
"variants":
|
|
1397
|
+
"variants": accepted_variants,
|
|
1289
1398
|
"score": old_entry.get("score", 1.0),
|
|
1290
1399
|
"recognizer": old_entry.get("recognizer", "unknown")
|
|
1291
1400
|
}
|
|
@@ -1633,7 +1742,7 @@ def write_document_index(
|
|
|
1633
1742
|
return written
|
|
1634
1743
|
|
|
1635
1744
|
|
|
1636
|
-
def
|
|
1745
|
+
def run_pipeline(resources: PipelineResources):
|
|
1637
1746
|
"""Main pipeline orchestration."""
|
|
1638
1747
|
parser = argparse.ArgumentParser(
|
|
1639
1748
|
description="Convert documents to Markdown and scan for sensitive data",
|
|
@@ -1697,6 +1806,7 @@ def main():
|
|
|
1697
1806
|
help="Reuse existing Markdown and scans whose source fingerprint is unchanged",
|
|
1698
1807
|
)
|
|
1699
1808
|
args = parser.parse_args()
|
|
1809
|
+
progress_context = {"offset": 0, "total": 0, "active_chunks": 0}
|
|
1700
1810
|
|
|
1701
1811
|
# Validate input files
|
|
1702
1812
|
input_files = []
|
|
@@ -1747,7 +1857,8 @@ def main():
|
|
|
1747
1857
|
|
|
1748
1858
|
# Start scanner worker immediately so model loading overlaps with Phase 1.
|
|
1749
1859
|
if scan_needed:
|
|
1750
|
-
scanner_worker = start_scanner_worker()
|
|
1860
|
+
resources.scanner_worker = start_scanner_worker()
|
|
1861
|
+
scanner_worker = resources.scanner_worker
|
|
1751
1862
|
else:
|
|
1752
1863
|
scanner_worker = None
|
|
1753
1864
|
print("✅ Every PII scan is already up to date — GLiNER not started")
|
|
@@ -1825,19 +1936,27 @@ def main():
|
|
|
1825
1936
|
if skipped_scans:
|
|
1826
1937
|
print(f"⏭️ {skipped_scans} file(s) already scanned, left untouched")
|
|
1827
1938
|
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1939
|
+
chunk_counts = {md_file: count_gliner_chunks(md_file) for md_file in pending_scans}
|
|
1940
|
+
total_chunks = sum(chunk_counts.values())
|
|
1941
|
+
progress_context["total"] = total_chunks
|
|
1942
|
+
|
|
1943
|
+
# Wait for the scanner worker to finish loading models. One retry cleans up
|
|
1944
|
+
# and relaunches a stalled/dead worker; a second failure aborts the job so
|
|
1945
|
+
# the heavyweight model is never reloaded once per document.
|
|
1946
|
+
worker_ready = wait_for_worker_ready(scanner_worker, progress_context) if pending_scans else False
|
|
1832
1947
|
|
|
1833
1948
|
if pending_scans and not worker_ready:
|
|
1834
1949
|
print("⚠️ Scanner worker not ready — cleaning up and retrying once", file=sys.stderr)
|
|
1835
1950
|
stop_scanner_worker(scanner_worker)
|
|
1836
|
-
scanner_worker = start_scanner_worker()
|
|
1837
|
-
|
|
1951
|
+
resources.scanner_worker = start_scanner_worker()
|
|
1952
|
+
scanner_worker = resources.scanner_worker
|
|
1953
|
+
worker_ready = wait_for_worker_ready(scanner_worker, progress_context)
|
|
1838
1954
|
|
|
1839
1955
|
if pending_scans and not worker_ready:
|
|
1840
|
-
|
|
1956
|
+
raise RuntimeError(
|
|
1957
|
+
"Scanner GLiNER indisponible après deux tentatives ; "
|
|
1958
|
+
"le traitement est arrêté pour éviter de recharger le modèle pour chaque fichier."
|
|
1959
|
+
)
|
|
1841
1960
|
print()
|
|
1842
1961
|
|
|
1843
1962
|
scan_success_count = 0
|
|
@@ -1846,18 +1965,26 @@ def main():
|
|
|
1846
1965
|
index_path = state_target.parent / "document-index.json"
|
|
1847
1966
|
# Raw detections contain PII. They live only in a private OS temporary
|
|
1848
1967
|
# directory and are deleted individually as each file is merged.
|
|
1849
|
-
scan_workspace = tempfile.TemporaryDirectory(prefix="piecemaker-scans-")
|
|
1968
|
+
resources.scan_workspace = tempfile.TemporaryDirectory(prefix="piecemaker-scans-")
|
|
1969
|
+
scan_workspace = resources.scan_workspace
|
|
1850
1970
|
scan_output_dir = scan_workspace.name
|
|
1851
1971
|
|
|
1972
|
+
emit_chunk_progress(0, total_chunks)
|
|
1973
|
+
completed_chunks = 0
|
|
1852
1974
|
for i, md_file in enumerate(pending_scans, start=1):
|
|
1853
|
-
print_progress("SCAN", i, len(pending_scans))
|
|
1854
1975
|
print(f"🔍 [{i}/{len(pending_scans)}] Scanning: {Path(md_file).name}")
|
|
1855
1976
|
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1977
|
+
progress_context["active_chunks"] = chunk_counts[md_file]
|
|
1978
|
+
success = scan_file_via_worker(
|
|
1979
|
+
scanner_worker,
|
|
1980
|
+
md_file,
|
|
1981
|
+
scan_output_dir,
|
|
1982
|
+
progress_context,
|
|
1983
|
+
completed_chunks,
|
|
1984
|
+
)
|
|
1985
|
+
if success:
|
|
1986
|
+
completed_chunks += chunk_counts[md_file]
|
|
1987
|
+
emit_chunk_progress(completed_chunks, total_chunks)
|
|
1861
1988
|
|
|
1862
1989
|
if not success:
|
|
1863
1990
|
print(f" ⚠️ Scan failed, markdown preserved")
|
|
@@ -1906,10 +2033,6 @@ def main():
|
|
|
1906
2033
|
scan_success_count += 1
|
|
1907
2034
|
print()
|
|
1908
2035
|
|
|
1909
|
-
# Shut down worker
|
|
1910
|
-
stop_scanner_worker(scanner_worker)
|
|
1911
|
-
scan_workspace.cleanup()
|
|
1912
|
-
|
|
1913
2036
|
print(f"✅ Phase 2 complete: {scan_success_count}/{len(pending_scans)} files scanned")
|
|
1914
2037
|
print()
|
|
1915
2038
|
|
|
@@ -1954,6 +2077,14 @@ def main():
|
|
|
1954
2077
|
return 1
|
|
1955
2078
|
|
|
1956
2079
|
|
|
2080
|
+
def main():
|
|
2081
|
+
resources = PipelineResources()
|
|
2082
|
+
try:
|
|
2083
|
+
return run_pipeline(resources)
|
|
2084
|
+
finally:
|
|
2085
|
+
resources.close()
|
|
2086
|
+
|
|
2087
|
+
|
|
1957
2088
|
if __name__ == "__main__":
|
|
1958
2089
|
try:
|
|
1959
2090
|
sys.exit(main())
|
|
Binary file
|