python-hwpx-automation 6.0.3__py3-none-any.whl
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.
- hwpx_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Stateless specialized automation handlers for the optional MCP adapter."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..core.content import (
|
|
13
|
+
collect_full_text,
|
|
14
|
+
)
|
|
15
|
+
from ..core.document import open_doc
|
|
16
|
+
from ..office.authoring.advanced_generators import (
|
|
17
|
+
build_image_grid as build_hwpx_image_grid,
|
|
18
|
+
)
|
|
19
|
+
from ..office.authoring.advanced_generators import (
|
|
20
|
+
build_meeting_nameplates as build_hwpx_meeting_nameplates,
|
|
21
|
+
)
|
|
22
|
+
from ..office.authoring.advanced_generators import (
|
|
23
|
+
build_organization_chart as build_hwpx_organization_chart,
|
|
24
|
+
)
|
|
25
|
+
from ..office.compliance import DEFAULT_POLICY, detect_pii, mask_value
|
|
26
|
+
from ..office.document_ops import (
|
|
27
|
+
build_mail_merge as build_hwpx_mail_merge,
|
|
28
|
+
)
|
|
29
|
+
from ..office.document_ops import (
|
|
30
|
+
inspect_mail_merge_placeholders as inspect_hwpx_mail_merge_placeholders,
|
|
31
|
+
)
|
|
32
|
+
from ..office.exam import (
|
|
33
|
+
ExamParseError,
|
|
34
|
+
FormProfileError,
|
|
35
|
+
compose_exam_into_form,
|
|
36
|
+
measure_question_splits,
|
|
37
|
+
)
|
|
38
|
+
from ..office.form_fill.fit import FitPolicy as _FitPolicy
|
|
39
|
+
from ..office.form_fill.fit import seal as seal_ops
|
|
40
|
+
from ..office.form_fill.fit.wordbox import (
|
|
41
|
+
OracleUnavailable,
|
|
42
|
+
extract_image_boxes,
|
|
43
|
+
render_glyph_boxes,
|
|
44
|
+
)
|
|
45
|
+
from ..office.rendering import NullOracle
|
|
46
|
+
from ..office.rendering import resolve_hancom_oracle as resolve_oracle
|
|
47
|
+
from ..storage import (
|
|
48
|
+
build_hwpx_open_safety_report,
|
|
49
|
+
)
|
|
50
|
+
from ..utils.helpers import resolve_path
|
|
51
|
+
from ._shared import (
|
|
52
|
+
_decode_image_base64,
|
|
53
|
+
_id_integrity_payload,
|
|
54
|
+
_revision_guard,
|
|
55
|
+
_save_doc_verification,
|
|
56
|
+
_with_dry_run_verification,
|
|
57
|
+
_with_save_verification,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Retain the established handler-module binding used by ownership consumers.
|
|
61
|
+
FitPolicy = _FitPolicy
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def inspect_mail_merge_placeholders(filename: str) -> dict:
|
|
65
|
+
"""메일머지 템플릿의 placeholder key를 확인합니다."""
|
|
66
|
+
if inspect_hwpx_mail_merge_placeholders is None:
|
|
67
|
+
raise RuntimeError("installed python-hwpx does not provide mail merge tools")
|
|
68
|
+
return inspect_hwpx_mail_merge_placeholders(resolve_path(filename))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _mail_merge_data_source(
|
|
72
|
+
data_rows: list | dict | None, data_filename: str | None
|
|
73
|
+
) -> Any:
|
|
74
|
+
if data_rows is not None:
|
|
75
|
+
return data_rows
|
|
76
|
+
if data_filename:
|
|
77
|
+
return resolve_path(data_filename)
|
|
78
|
+
raise ValueError("provide data_rows or data_filename")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _mail_merge_open_safety_summary(report: dict) -> dict:
|
|
82
|
+
row_reports = list(report.get("rows") or [])
|
|
83
|
+
checked = 0
|
|
84
|
+
failures: list[dict[str, Any]] = []
|
|
85
|
+
for row in row_reports:
|
|
86
|
+
open_safety = row.get("openSafety")
|
|
87
|
+
if not isinstance(open_safety, dict):
|
|
88
|
+
continue
|
|
89
|
+
checked += 1
|
|
90
|
+
if not bool(open_safety.get("ok")):
|
|
91
|
+
failures.append(
|
|
92
|
+
{
|
|
93
|
+
"rowIndex": row.get("rowIndex"),
|
|
94
|
+
"filename": row.get("filename"),
|
|
95
|
+
"summary": open_safety.get("summary"),
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
return {
|
|
99
|
+
"ok": checked == int(report.get("createdCount", 0)) and not failures,
|
|
100
|
+
"checkedCount": checked,
|
|
101
|
+
"failureCount": len(failures),
|
|
102
|
+
"failures": failures,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def mail_merge(
|
|
107
|
+
template_filename: str,
|
|
108
|
+
data_rows: list | dict = None, # type: ignore[assignment] # Frozen ToolSpec default.
|
|
109
|
+
data_filename: str = None,
|
|
110
|
+
output_dir: str = None,
|
|
111
|
+
filename_pattern: str = "{index:03d}.hwpx",
|
|
112
|
+
zip_filename: str = None,
|
|
113
|
+
strict: bool = False,
|
|
114
|
+
split_newlines: bool = True,
|
|
115
|
+
fit_mode: str | None = None,
|
|
116
|
+
max_lines: int = 1,
|
|
117
|
+
) -> dict:
|
|
118
|
+
"""템플릿 HWPX와 CSV/JSON/XLSX(명부)/rows 데이터로 N부를 생성합니다.
|
|
119
|
+
|
|
120
|
+
``fit_mode`` (keep·wrap·shrink·wrap_then_shrink·expand_row·truncate_with_report·
|
|
121
|
+
fail_on_overflow)를 주면 **fit-aware** 배치가 됩니다: 각 placeholder 슬롯을 템플릿에서
|
|
122
|
+
한 번 측정(template-once-measure)하고, 슬롯을 넘치거나 필수값이 빠진 레코드를
|
|
123
|
+
``needsReview[]``/``skipped[]`` 로 격리합니다(나머지 배치는 안전). zip/openSafety
|
|
124
|
+
evidence 동봉.
|
|
125
|
+
"""
|
|
126
|
+
if build_hwpx_mail_merge is None:
|
|
127
|
+
raise RuntimeError("installed python-hwpx does not provide mail merge tools")
|
|
128
|
+
data_source = _mail_merge_data_source(data_rows, data_filename)
|
|
129
|
+
report = build_hwpx_mail_merge(
|
|
130
|
+
resolve_path(template_filename),
|
|
131
|
+
data_source,
|
|
132
|
+
output_dir=resolve_path(output_dir) if output_dir else None,
|
|
133
|
+
filename_pattern=filename_pattern,
|
|
134
|
+
zip_path=resolve_path(zip_filename) if zip_filename else None,
|
|
135
|
+
strict=strict,
|
|
136
|
+
split_newlines=split_newlines,
|
|
137
|
+
fit_mode=fit_mode,
|
|
138
|
+
max_lines=max_lines,
|
|
139
|
+
)
|
|
140
|
+
open_safety = _mail_merge_open_safety_summary(report)
|
|
141
|
+
report["openSafety"] = open_safety
|
|
142
|
+
report["verification"] = {
|
|
143
|
+
"openSafety": open_safety,
|
|
144
|
+
"createdCount": report.get("createdCount", 0),
|
|
145
|
+
"rowCount": report.get("rowCount", 0),
|
|
146
|
+
"rowsWithIssues": report.get("rowsWithIssues", []),
|
|
147
|
+
"zip": report.get("zip"),
|
|
148
|
+
}
|
|
149
|
+
return report
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _single_block_plan(block: dict, *, title: str = "") -> dict:
|
|
153
|
+
return {
|
|
154
|
+
"schemaVersion": "hwpx.document_plan.v2",
|
|
155
|
+
"title": title,
|
|
156
|
+
"sections": [{"blocks": [block]}],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def build_image_grid(
|
|
161
|
+
images: list,
|
|
162
|
+
columns: int = 2,
|
|
163
|
+
image_width_mm: float = None,
|
|
164
|
+
title: str = "사진대지",
|
|
165
|
+
) -> dict:
|
|
166
|
+
"""사진 목록을 plan v2 image_grid block과 생성 가능한 document_plan으로 변환합니다."""
|
|
167
|
+
if build_hwpx_image_grid is None:
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
"installed python-hwpx does not provide image_grid generator"
|
|
170
|
+
)
|
|
171
|
+
block = build_hwpx_image_grid(
|
|
172
|
+
images or [],
|
|
173
|
+
columns=columns,
|
|
174
|
+
image_width_mm=image_width_mm,
|
|
175
|
+
)
|
|
176
|
+
return {
|
|
177
|
+
"block": block,
|
|
178
|
+
"document_plan": _single_block_plan(block, title=title),
|
|
179
|
+
"next_tool": "create_document_from_plan",
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def build_meeting_nameplates(
|
|
184
|
+
names: list[str],
|
|
185
|
+
size: str = "150x70",
|
|
186
|
+
columns: int = 2,
|
|
187
|
+
title: str = "회의 명패",
|
|
188
|
+
) -> dict:
|
|
189
|
+
"""참석자 명단을 회의 명패 table block과 document_plan으로 변환합니다."""
|
|
190
|
+
if build_hwpx_meeting_nameplates is None:
|
|
191
|
+
raise RuntimeError(
|
|
192
|
+
"installed python-hwpx does not provide meeting nameplate generator"
|
|
193
|
+
)
|
|
194
|
+
block = build_hwpx_meeting_nameplates(names or [], size=size, columns=columns)
|
|
195
|
+
return {
|
|
196
|
+
"block": block,
|
|
197
|
+
"document_plan": _single_block_plan(block, title=title),
|
|
198
|
+
"next_tool": "create_document_from_plan",
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def build_organization_chart(
|
|
203
|
+
hierarchy: dict | list,
|
|
204
|
+
max_depth: int = 3,
|
|
205
|
+
title: str = "조직도",
|
|
206
|
+
) -> dict:
|
|
207
|
+
"""2~3단 계층 데이터를 표 기반 조직도 block과 document_plan으로 변환합니다."""
|
|
208
|
+
if build_hwpx_organization_chart is None:
|
|
209
|
+
raise RuntimeError(
|
|
210
|
+
"installed python-hwpx does not provide organization chart generator"
|
|
211
|
+
)
|
|
212
|
+
block = build_hwpx_organization_chart(hierarchy or {}, max_depth=max_depth)
|
|
213
|
+
return {
|
|
214
|
+
"block": block,
|
|
215
|
+
"document_plan": _single_block_plan(block, title=title),
|
|
216
|
+
"next_tool": "create_document_from_plan",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def scan_personal_info(filename: str | None = None, text: str | None = None) -> dict:
|
|
221
|
+
"""문서/텍스트의 개인정보(PII)를 탐지하는 read-only 감사 (원본값 미노출).
|
|
222
|
+
|
|
223
|
+
기계검증 세트(주민등록번호·휴대폰·이메일·카드)는 항상 high-confidence, 맥락형(계좌·주소·이름)은
|
|
224
|
+
라벨 게이트 low-confidence. 반환은 유형별 건수 + **마스킹된 예시만** — 원본 PII는 절대 반환하지 않습니다.
|
|
225
|
+
마스킹은 따로 하지 않고 이 도구는 탐지/감사만 합니다(마스킹은 fill/merge/extract 도구의 `mask` 기본 ON).
|
|
226
|
+
"""
|
|
227
|
+
if text is None:
|
|
228
|
+
if not filename:
|
|
229
|
+
return {"error": "filename 또는 text 중 하나가 필요합니다."}
|
|
230
|
+
text = collect_full_text(open_doc(resolve_path(filename)))
|
|
231
|
+
spans = detect_pii(text or "", DEFAULT_POLICY)
|
|
232
|
+
by_type: dict[str, dict] = {}
|
|
233
|
+
for span in spans:
|
|
234
|
+
kind = span["type"]
|
|
235
|
+
bucket = by_type.setdefault(
|
|
236
|
+
kind,
|
|
237
|
+
{
|
|
238
|
+
"type": kind,
|
|
239
|
+
"confidence": span["confidence"],
|
|
240
|
+
"count": 0,
|
|
241
|
+
"maskedExamples": [],
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
bucket["count"] += 1
|
|
245
|
+
if len(bucket["maskedExamples"]) < 3:
|
|
246
|
+
bucket["maskedExamples"].append(
|
|
247
|
+
mask_value(span["value"], kind, DEFAULT_POLICY)
|
|
248
|
+
)
|
|
249
|
+
return {
|
|
250
|
+
"report_version": "pii-scan-v1",
|
|
251
|
+
"total": len(spans),
|
|
252
|
+
"byType": list(by_type.values()),
|
|
253
|
+
"machineSet": ["rrn", "phone", "email", "card"],
|
|
254
|
+
"note": "기계세트=high-confidence 항상 탐지; 맥락형(account/address/name)=라벨게이트 low-confidence. 원본값 미반환(마스킹 예시만).",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _nearest_rect(rects: list, center: tuple[float, float]):
|
|
259
|
+
cx, cy = center
|
|
260
|
+
return min(
|
|
261
|
+
rects,
|
|
262
|
+
key=lambda r: ((r.x0 + r.x1) / 2 - cx) ** 2 + ((r.y0 + r.y1) / 2 - cy) ** 2,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _check_seal_compliance_impl(
|
|
267
|
+
path: str,
|
|
268
|
+
sender_text: str,
|
|
269
|
+
*,
|
|
270
|
+
tol_pt: float,
|
|
271
|
+
expected_center: tuple[float, float] | None = None,
|
|
272
|
+
) -> dict[str, Any]:
|
|
273
|
+
"""Render *path* via the Hancom oracle and decide the 직인 rule (pass/fail).
|
|
274
|
+
|
|
275
|
+
The seal is a picture, so it is located with ``extract_image_boxes`` (not text);
|
|
276
|
+
the 발신명의 anchor is found in the same render. Degrades to
|
|
277
|
+
``renderChecked=False`` when no oracle is reachable — never a silent pass.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
fd, pdf = tempfile.mkstemp(prefix="seal_verify_", suffix=".pdf")
|
|
281
|
+
os.close(fd)
|
|
282
|
+
try:
|
|
283
|
+
boxes, _sizes, backend = render_glyph_boxes(path, out_pdf=pdf)
|
|
284
|
+
seal_rects = extract_image_boxes(pdf)
|
|
285
|
+
except OracleUnavailable as exc:
|
|
286
|
+
return {
|
|
287
|
+
"ok": False,
|
|
288
|
+
"renderChecked": False,
|
|
289
|
+
"note": "한컴 오라클이 없어 직인 배치를 검증할 수 없습니다.",
|
|
290
|
+
"reason": str(exc),
|
|
291
|
+
}
|
|
292
|
+
finally:
|
|
293
|
+
try:
|
|
294
|
+
os.unlink(pdf)
|
|
295
|
+
except OSError:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
if not seal_rects:
|
|
299
|
+
return {
|
|
300
|
+
"ok": False,
|
|
301
|
+
"renderChecked": True,
|
|
302
|
+
"note": "렌더에 직인 이미지가 없습니다 (isEmbeded 누락 또는 미배치).",
|
|
303
|
+
}
|
|
304
|
+
anchor = seal_ops.find_seal_anchor(boxes, sender_text)
|
|
305
|
+
center = expected_center or (anchor.center if anchor is not None else None)
|
|
306
|
+
seal_rect = (
|
|
307
|
+
_nearest_rect(seal_rects, center) if center is not None else seal_rects[-1]
|
|
308
|
+
)
|
|
309
|
+
verdict = seal_ops.check_seal_placement(
|
|
310
|
+
boxes, seal_rect, sender_text, tol_pt=tol_pt
|
|
311
|
+
)
|
|
312
|
+
out: dict[str, Any] = {"ok": verdict.ok, "renderChecked": True, "backend": backend}
|
|
313
|
+
out.update(verdict.to_dict())
|
|
314
|
+
return out
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def place_seal(
|
|
318
|
+
filename: str,
|
|
319
|
+
sender_text: str,
|
|
320
|
+
image_base64: str,
|
|
321
|
+
image_format: str = "png",
|
|
322
|
+
seal_width_mm: float = 25.0,
|
|
323
|
+
seal_height_mm: float | None = None,
|
|
324
|
+
anchor_x: float | None = None,
|
|
325
|
+
anchor_y: float | None = None,
|
|
326
|
+
anchor_page: int | None = None,
|
|
327
|
+
verify: bool = True,
|
|
328
|
+
tol_pt: float = 6.0,
|
|
329
|
+
output: str | None = None,
|
|
330
|
+
dry_run: bool = False,
|
|
331
|
+
expected_revision: str = None,
|
|
332
|
+
) -> dict:
|
|
333
|
+
"""발신명의(issuer line) 끝글자에 직인/관인을 floating으로 찍습니다 (FR-003).
|
|
334
|
+
|
|
335
|
+
한컴 렌더 오라클로 발신명의 위치(앵커)를 찾아 직인을 그 위에 스탬프합니다
|
|
336
|
+
(textWrap=IN_FRONT_OF_TEXT — 겹친 글자를 밀지 않음). 오라클이 없으면 ``anchor_x``/
|
|
337
|
+
``anchor_y`` 로 PDF 포인트 앵커를 직접 지정할 수 있고, 둘 다 없으면
|
|
338
|
+
``renderChecked=false`` 로 정직하게 degrade 합니다(임의 배치 금지). ``verify=True``
|
|
339
|
+
이면 저장 후 재렌더로 직인이 발신명의에 규칙대로 찍혔는지 검증합니다.
|
|
340
|
+
"""
|
|
341
|
+
path = resolve_path(filename)
|
|
342
|
+
guard = _revision_guard(path, expected_revision)
|
|
343
|
+
if guard is not None:
|
|
344
|
+
return guard
|
|
345
|
+
target_path = resolve_path(output) if output else path
|
|
346
|
+
image_data = _decode_image_base64(image_base64)
|
|
347
|
+
|
|
348
|
+
# 1) locate the 발신명의 anchor — explicit override, else the render oracle.
|
|
349
|
+
if anchor_x is not None and anchor_y is not None:
|
|
350
|
+
anchor_center = (float(anchor_x), float(anchor_y))
|
|
351
|
+
anchor_page_resolved = anchor_page
|
|
352
|
+
anchor_source = "explicit"
|
|
353
|
+
else:
|
|
354
|
+
try:
|
|
355
|
+
boxes, _sizes, backend = render_glyph_boxes(path)
|
|
356
|
+
except OracleUnavailable as exc:
|
|
357
|
+
return {
|
|
358
|
+
"ok": False,
|
|
359
|
+
"filename": filename,
|
|
360
|
+
"renderChecked": False,
|
|
361
|
+
"note": "한컴 오라클이 없어 발신명의 위치를 찾을 수 없습니다. anchor_x/anchor_y(PDF pt)로 직접 지정하세요.",
|
|
362
|
+
"reason": str(exc),
|
|
363
|
+
}
|
|
364
|
+
anchor = seal_ops.find_seal_anchor(boxes, sender_text)
|
|
365
|
+
if anchor is None:
|
|
366
|
+
return {
|
|
367
|
+
"ok": False,
|
|
368
|
+
"filename": filename,
|
|
369
|
+
"renderChecked": True,
|
|
370
|
+
"note": f"발신명의 '{sender_text}'를 렌더에서 찾지 못했습니다.",
|
|
371
|
+
}
|
|
372
|
+
anchor_center = anchor.center
|
|
373
|
+
anchor_page_resolved = anchor.glyph.page
|
|
374
|
+
anchor_source = backend
|
|
375
|
+
|
|
376
|
+
# 2) stamp the floating seal on the anchor.
|
|
377
|
+
doc = open_doc(path)
|
|
378
|
+
placement = seal_ops.place_seal(
|
|
379
|
+
doc,
|
|
380
|
+
image_data=image_data,
|
|
381
|
+
image_format=image_format,
|
|
382
|
+
sender_text=sender_text,
|
|
383
|
+
anchor_center_pt=anchor_center,
|
|
384
|
+
seal_width_mm=seal_width_mm,
|
|
385
|
+
seal_height_mm=seal_height_mm,
|
|
386
|
+
page=anchor_page_resolved,
|
|
387
|
+
)
|
|
388
|
+
if not placement.placed:
|
|
389
|
+
return {
|
|
390
|
+
"ok": False,
|
|
391
|
+
"filename": filename,
|
|
392
|
+
"placement": placement.to_dict(),
|
|
393
|
+
"note": placement.note or "발신명의 문단을 찾지 못했습니다.",
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
result = {
|
|
397
|
+
"ok": True,
|
|
398
|
+
"filename": filename,
|
|
399
|
+
"outputPath": target_path,
|
|
400
|
+
"anchorSource": anchor_source,
|
|
401
|
+
"placement": placement.to_dict(),
|
|
402
|
+
"idIntegrity": _id_integrity_payload(doc),
|
|
403
|
+
}
|
|
404
|
+
if dry_run:
|
|
405
|
+
return _with_dry_run_verification(result, doc, target_path)
|
|
406
|
+
verification = _save_doc_verification(doc, target_path)
|
|
407
|
+
result = _with_save_verification(result, verification)
|
|
408
|
+
|
|
409
|
+
# 3) optional oracle re-verify against the realized seal center.
|
|
410
|
+
if verify:
|
|
411
|
+
expected_center = (
|
|
412
|
+
(placement.horz_offset + placement.seal_width_hu / 2) / 100.0,
|
|
413
|
+
(placement.vert_offset + placement.seal_height_hu / 2) / 100.0,
|
|
414
|
+
)
|
|
415
|
+
result["sealVerdict"] = _check_seal_compliance_impl(
|
|
416
|
+
target_path, sender_text, tol_pt=tol_pt, expected_center=expected_center
|
|
417
|
+
)
|
|
418
|
+
return result
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def check_seal_compliance(
|
|
422
|
+
filename: str,
|
|
423
|
+
sender_text: str,
|
|
424
|
+
tol_pt: float = 6.0,
|
|
425
|
+
) -> dict:
|
|
426
|
+
"""직인이 발신명의 끝글자에 규칙대로 찍혔는지 pass/fail 검사 (FR-003).
|
|
427
|
+
|
|
428
|
+
한컴으로 렌더해 직인 이미지의 실제 위치를 발신명의 앵커와 비교합니다(중심 tol 이내 +
|
|
429
|
+
의도치 않은 글자 가림 없음). 평가자가 그대로 돌릴 수 있는 차별적 검사 — 잘 찍힌 직인은
|
|
430
|
+
pass, 어긋난 직인은 fail. 오라클이 없으면 ``renderChecked=false`` 로 degrade 합니다.
|
|
431
|
+
"""
|
|
432
|
+
path = resolve_path(filename)
|
|
433
|
+
return _check_seal_compliance_impl(path, sender_text, tol_pt=tol_pt)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def compose_exam(
|
|
437
|
+
form_filename: str,
|
|
438
|
+
output: str,
|
|
439
|
+
exam_md: str | None = None,
|
|
440
|
+
exam_md_filename: str | None = None,
|
|
441
|
+
max_rounds: int = 2,
|
|
442
|
+
verify: bool = True,
|
|
443
|
+
role_style_names: dict[str, str] | None = None,
|
|
444
|
+
) -> dict:
|
|
445
|
+
"""출제 md를 학교 양식 .hwpx에 그 양식의 기존 스타일로 재조판합니다 (시험지 조판).
|
|
446
|
+
|
|
447
|
+
각 문항이 단/쪽 경계에서 잘리지 않게(keep-together) 하고, 관리박스·머리글/꼬리글은
|
|
448
|
+
무손실 보존하며, [그림N]/[표N]/[식N]은 텍스트 placeholder로 남깁니다(사람이 그림을
|
|
449
|
+
나중에 삽입). ``exam_md``(인라인 문자열) 또는 ``exam_md_filename``(경로) 중 정확히
|
|
450
|
+
하나를 줍니다. ``verify=True``(기본)면 한컴 렌더로 문항-split/overflow/placeholder를
|
|
451
|
+
검증하고 — 오라클이 없으면 ``renderChecked=false`` 로 정직하게 degrade — ``verify=False``
|
|
452
|
+
면 렌더 없이 조판만 합니다. 한컴이 본문을 벡터 커브로 export 하는 양식은
|
|
453
|
+
``splits=null`` + ``needsReview=true`` 로 정직 보고합니다(렌더 이미지로 시각 검증 필요).
|
|
454
|
+
잘못된 md/양식은 조용히 틀린 문서를 내지 않고 ``ok=false`` 로 실패합니다.
|
|
455
|
+
"""
|
|
456
|
+
if compose_exam_into_form is None:
|
|
457
|
+
return {
|
|
458
|
+
"ok": False,
|
|
459
|
+
"filename": form_filename,
|
|
460
|
+
"renderChecked": False,
|
|
461
|
+
"needsReview": True,
|
|
462
|
+
"note": "이 python-hwpx 빌드에는 hwpx.exam 조판 모듈이 없습니다.",
|
|
463
|
+
}
|
|
464
|
+
if (exam_md is None) == (exam_md_filename is None):
|
|
465
|
+
return {
|
|
466
|
+
"ok": False,
|
|
467
|
+
"filename": form_filename,
|
|
468
|
+
"renderChecked": False,
|
|
469
|
+
"needsReview": True,
|
|
470
|
+
"note": "exam_md(인라인) 또는 exam_md_filename(경로) 중 정확히 하나를 지정하세요.",
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
form_path = resolve_path(form_filename)
|
|
474
|
+
if exam_md_filename is not None:
|
|
475
|
+
try:
|
|
476
|
+
exam_md = Path(resolve_path(exam_md_filename)).read_text(encoding="utf-8")
|
|
477
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
478
|
+
return {
|
|
479
|
+
"ok": False,
|
|
480
|
+
"filename": form_filename,
|
|
481
|
+
"renderChecked": False,
|
|
482
|
+
"needsReview": True,
|
|
483
|
+
"error": type(exc).__name__,
|
|
484
|
+
"note": f"exam_md_filename을 읽을 수 없습니다: {exc}",
|
|
485
|
+
}
|
|
486
|
+
out_path = resolve_path(output)
|
|
487
|
+
oracle = None if verify else NullOracle()
|
|
488
|
+
assert exam_md is not None # narrowed by the XOR guard and file-load branch
|
|
489
|
+
try:
|
|
490
|
+
result = compose_exam_into_form(
|
|
491
|
+
form_path,
|
|
492
|
+
exam_md,
|
|
493
|
+
out_path,
|
|
494
|
+
oracle=oracle,
|
|
495
|
+
max_rounds=max_rounds,
|
|
496
|
+
role_style_names=role_style_names,
|
|
497
|
+
)
|
|
498
|
+
except (ExamParseError, FormProfileError) as exc:
|
|
499
|
+
return {
|
|
500
|
+
"ok": False,
|
|
501
|
+
"filename": form_filename,
|
|
502
|
+
"renderChecked": False,
|
|
503
|
+
"needsReview": True,
|
|
504
|
+
"error": type(exc).__name__,
|
|
505
|
+
"note": str(exc),
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
payload = {
|
|
509
|
+
"ok": True,
|
|
510
|
+
"filename": form_filename,
|
|
511
|
+
"outputPath": out_path,
|
|
512
|
+
"renderChecked": result.render_checked,
|
|
513
|
+
"splits": result.splits,
|
|
514
|
+
"overflow": result.overflow,
|
|
515
|
+
"placeholdersOk": result.placeholders_ok,
|
|
516
|
+
"rounds": result.rounds,
|
|
517
|
+
"needsReview": result.needs_review,
|
|
518
|
+
"notes": list(result.notes),
|
|
519
|
+
}
|
|
520
|
+
payload["openSafety"] = build_hwpx_open_safety_report(Path(out_path))
|
|
521
|
+
return payload
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def verify_question_splits(
|
|
525
|
+
filename: str,
|
|
526
|
+
valid_question_numbers: list[str] | None = None,
|
|
527
|
+
marker_regex: str | None = None,
|
|
528
|
+
) -> dict:
|
|
529
|
+
"""한컴 렌더로 시험지 .hwpx의 문항이 단/쪽 경계에서 잘렸는지(문항-split) 검증합니다.
|
|
530
|
+
|
|
531
|
+
오라클이 없으면 ``renderChecked=false`` 로 정직하게 degrade 합니다(임의 0 금지).
|
|
532
|
+
한컴이 본문을 벡터 커브로 export 해 추출 텍스트에 조판 문항이 0개면(학교 원안지
|
|
533
|
+
양식에서 관측) ``splits=null`` + ``needsReview=true`` 로 보고하고 렌더 이미지 기반
|
|
534
|
+
시각 검증을 요구합니다. ``valid_question_numbers`` 로 측정 대상 문항을 한정하면 양식
|
|
535
|
+
chrome(예: "2026." 연도)이 가짜 문항 블록을 열지 않습니다.
|
|
536
|
+
"""
|
|
537
|
+
if measure_question_splits is None:
|
|
538
|
+
return {
|
|
539
|
+
"ok": False,
|
|
540
|
+
"filename": filename,
|
|
541
|
+
"renderChecked": False,
|
|
542
|
+
"needsReview": True,
|
|
543
|
+
"note": "이 python-hwpx 빌드에는 hwpx.exam 조판/측정 모듈이 없습니다.",
|
|
544
|
+
}
|
|
545
|
+
marker_re = None
|
|
546
|
+
if marker_regex:
|
|
547
|
+
try:
|
|
548
|
+
marker_re = re.compile(marker_regex)
|
|
549
|
+
except re.error as exc:
|
|
550
|
+
return {
|
|
551
|
+
"ok": False,
|
|
552
|
+
"filename": filename,
|
|
553
|
+
"renderChecked": False,
|
|
554
|
+
"splits": None,
|
|
555
|
+
"needsReview": True,
|
|
556
|
+
"error": "InvalidRegex",
|
|
557
|
+
"note": f"marker_regex가 올바른 정규식이 아닙니다: {exc}",
|
|
558
|
+
}
|
|
559
|
+
path = resolve_path(filename)
|
|
560
|
+
oracle = resolve_oracle()
|
|
561
|
+
if not oracle.available():
|
|
562
|
+
return {
|
|
563
|
+
"ok": True,
|
|
564
|
+
"filename": filename,
|
|
565
|
+
"renderChecked": False,
|
|
566
|
+
"splits": None,
|
|
567
|
+
"needsReview": True,
|
|
568
|
+
"note": "한컴 오라클이 없어 문항 split을 측정할 수 없습니다 (renderChecked=false).",
|
|
569
|
+
}
|
|
570
|
+
pdf = oracle.render_pdf(path)
|
|
571
|
+
if not pdf:
|
|
572
|
+
return {
|
|
573
|
+
"ok": False,
|
|
574
|
+
"filename": filename,
|
|
575
|
+
"renderChecked": False,
|
|
576
|
+
"splits": None,
|
|
577
|
+
"needsReview": True,
|
|
578
|
+
"note": "한컴 렌더가 PDF를 생성하지 못했습니다 (renderChecked=false).",
|
|
579
|
+
}
|
|
580
|
+
kwargs: dict[str, Any] = {}
|
|
581
|
+
if valid_question_numbers is not None:
|
|
582
|
+
kwargs["valid_ids"] = {str(n) for n in valid_question_numbers}
|
|
583
|
+
if marker_re is not None:
|
|
584
|
+
kwargs["marker_re"] = marker_re
|
|
585
|
+
report = measure_question_splits(pdf, **kwargs)
|
|
586
|
+
if report.n_blocks == 0:
|
|
587
|
+
return {
|
|
588
|
+
"ok": True,
|
|
589
|
+
"filename": filename,
|
|
590
|
+
"renderChecked": True,
|
|
591
|
+
"splits": None,
|
|
592
|
+
"needsReview": True,
|
|
593
|
+
"nBlocks": 0,
|
|
594
|
+
"note": (
|
|
595
|
+
"추출 가능한 텍스트 레이어에서 조판 문항을 찾지 못했습니다(벡터 커브 export 양식"
|
|
596
|
+
" 추정). 문항-split은 텍스트 게이트로 검증할 수 없습니다 — 렌더 이미지로 시각"
|
|
597
|
+
" 검증이 필요합니다."
|
|
598
|
+
),
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
"ok": True,
|
|
602
|
+
"filename": filename,
|
|
603
|
+
"renderChecked": True,
|
|
604
|
+
"splits": report.n_splits,
|
|
605
|
+
"kinds": dict(report.kinds),
|
|
606
|
+
"splitIds": list(report.split_ids),
|
|
607
|
+
"nBlocks": report.n_blocks,
|
|
608
|
+
"nGlyphs": report.n_glyphs,
|
|
609
|
+
"needsReview": report.n_splits > 0,
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
__all__ = [
|
|
614
|
+
"build_image_grid",
|
|
615
|
+
"build_meeting_nameplates",
|
|
616
|
+
"build_organization_chart",
|
|
617
|
+
"check_seal_compliance",
|
|
618
|
+
"compose_exam",
|
|
619
|
+
"inspect_mail_merge_placeholders",
|
|
620
|
+
"mail_merge",
|
|
621
|
+
"place_seal",
|
|
622
|
+
"scan_personal_info",
|
|
623
|
+
"verify_question_splits",
|
|
624
|
+
]
|