
Nature Paper To Patent
- 3.5k installs
- 33.4k repo stars
- Updated August 4, 2026
- yuan1z0825/nature-skills
Helps with ai & agent building tasks.
About
nature-paper-to-patent is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- nature-paper-to-patent
- AI & Agent Building
- AI-coding skill
Nature Paper To Patent by the numbers
- 3,458 all-time installs (skills.sh)
- +882 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #232 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yuan1z0825/nature-skills --skill nature-paper-to-patentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3.5k |
|---|---|
| repo stars | ★ 33.4k |
| Last updated | August 4, 2026 |
| Repository | yuan1z0825/nature-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Paper to Chinese Patent
Use this file as the router for the patent-drafting workflow. Do not draft the application directly from the paper abstract or contribution list.
1. Load the workflow
Read manifest.yaml, then read every file under always_load.
Detect these axes from the user's files and request:
source_format: selectable PDF, scanned PDF, pasted text, or mixed project;task_mode: full draft, claim set, disclosure analysis, or paper-patent audit;invention_type: algorithm/software, apparatus/system, process/material, or mixed.
State the detected values in one short line. Load only the matching fragments declared in the manifest. Load detailed references only when their condition applies.
2. Preserve source grounding
Create stable source IDs before drafting:
P001...for paper text blocks;E001...for equations;F001...for source figures;C001...for source-code or supplementary evidence.
Every material feature in a formal claim must map to one or more source IDs. Use only explicit, inherent, needs-confirmation, or unsupported as support states. Exclude unsupported features from formal claims.
Never infer inventorship, ownership, unpublished implementation details, publication dates, prior-art conclusions, or legal sufficiency. Use [TO CONFIRM: specific question] outside formal claims when facts are missing.
3. Draft through stage gates
Complete the stages in static/core/workflow.md in order. Persist the intermediate artifacts specified there. Do not move to formal claims until the source map, terminology ledger, inventories, evidence ledger, and invention concept pass their gates.
For a full application, draft claims first, then align the specification, figures, embodiments, and abstract to the claim terminology and step order.
4. Produce Chinese formal documents
Agent-facing analysis may use the user's preferred language. Produce formal Chinese patent deliverables in Chinese:
- 权利要求书;
- 说明书;
- 说明书摘要;
- 摘要附图;
- figure labels and descriptions.
For algorithmic inventions, retain source-supported core formulas, define every symbol, explain each formula's technical operation, and render formulas as native editable Office Math in DOCX. Do not use plain LaTeX strings as the visible formula.
Generate the main flowchart from the ordered steps of the principal method claim. Its final node must name the concrete domain output, such as a defect detection result, target pose, state estimate, or control instruction. Reuse the same main figure as the abstract figure and a specification figure.
5. Validate before delivery
Populate the structured draft described in references/draft-schema.md, then run:
python scripts/validate_patent_draft.py draft.json
python scripts/build_patent_package.py draft.json --output-dir outputs --prefix patentResolve all validation ERROR findings. Review every WARNING against the source. Label the result incomplete draft when a required quality threshold in static/core/output-contract.md is not met.
The generated package is a drafting aid for inventor and patent-professional review, not a patentability opinion, infringement opinion, or filing guarantee.
{
"skill_name": "nature-paper-to-patent",
"evals": [
{
"id": 1,
"prompt": "分析一篇包含模型结构、损失函数和检测流程的PDF论文,生成完整的中国发明专利初稿。分别输出权利要求书、说明书、说明书摘要和摘要附图。所有核心公式必须是Word原生可编辑公式,每项权利要求特征必须能回溯到论文页码或公式。",
"expected_output": "A complete Chinese patent package with stable source IDs, evidence ledger, claim-feature map, editable equations, a claim-aligned main flowchart, methodology figures, separate DOCX files, and passing validation reports.",
"files": []
},
{
"id": 2,
"prompt": "论文是扫描版PDF,部分公式OCR不清楚。请先整理可确认的技术方案并生成权利要求草案,不确定的公式符号不要猜。",
"expected_output": "An OCR-aware claim draft that marks uncertain source blocks, excludes unsupported limitations from formal claims, lists specific inventor questions, and does not fabricate equation symbols.",
"files": []
},
{
"id": 3,
"prompt": "对照论文和一份已有专利,逐项判断专利权利要求在论文中是否有支持,并标注论文独有、专利独有、专利更宽或更窄的内容。",
"expected_output": "Separate source maps plus a bidirectional feature table using the declared relationship labels, exact locators for both documents, and no assumption that patent-only text is paper-supported.",
"files": []
}
]
}
name: nature-paper-to-patent
version: 1.0.0
description: >
Agent-neutral routing manifest for converting research disclosures into
evidence-grounded Chinese invention patent drafts.
always_load:
- static/core/principles.md
- static/core/workflow.md
- static/core/output-contract.md
axes:
source_format:
detect: >
Use pdf-text for a selectable-text PDF, scanned-pdf for an image-only or
OCR-required PDF, pasted-text for user-provided prose, and mixed-project
when the workspace also contains source code, supplementary files,
inventor notes, figures, or an existing patent.
values:
pdf-text: static/fragments/source/pdf-text.md
scanned-pdf: static/fragments/source/scanned-pdf.md
pasted-text: static/fragments/source/pasted-text.md
mixed-project: static/fragments/source/mixed-project.md
default: pdf-text
multi: true
task_mode:
detect: >
Use full-draft for a complete application package, claim-set for claims
only, disclosure-analysis for patentability-oriented technical
extraction, and paper-patent-audit for comparison with an existing
patent.
values:
full-draft: static/fragments/task/full-draft.md
claim-set: static/fragments/task/claim-set.md
disclosure-analysis: static/fragments/task/disclosure-analysis.md
paper-patent-audit: static/fragments/task/paper-patent-audit.md
default: full-draft
multi: false
invention_type:
detect: >
Determine the dominant technical subject. Use algorithm-software for
data processing, AI, signal processing, control, or computer-implemented
methods; apparatus-system for physical structures or cooperating
devices; process-material for manufacturing, chemistry, or material
preparation; and mixed when no single category is sufficient.
values:
algorithm-software: static/fragments/invention/algorithm-software.md
apparatus-system: static/fragments/invention/apparatus-system.md
process-material: static/fragments/invention/process-material.md
mixed: static/fragments/invention/mixed.md
default: algorithm-software
multi: false
references:
on_demand:
- condition: drafting or auditing Chinese claims and specification
path: references/cn-patent-drafting-guide.md
- condition: creating figures or selecting a paper methodology figure
path: references/patent-figure-guide.md
- condition: populating or validating the structured draft
path: references/draft-schema.md
- condition: learning transformations from paired paper-patent examples
path: references/corpus-derived-patterns.md
- condition: comparing a supplied paper with an existing patent
path: references/corpus-pair-audit.md
nature-paper-to-patent 技能
nature-paper-to-patent 是一个有证据约束的工作流,用于把科研论文、学位论文、技术报告、源代码、图表和发明人笔记转换为结构化中国发明专利草稿。
该技能面向研究生、高校科研人员和技术团队。它生成中文正式专利文档,同时让面向 agent 的分析和路由说明保持可跨 AI agent 迁移。
功能
- 将论文正文、公式、图表、代码和补充证据映射到稳定 source ID。
- 提取技术问题、协同技术手段、实施链条和具体技术输出。
- 在起草权利要求前建立 evidence ledger。
- 将每个实质性权利要求特征映射到来源证据。
- 支持完整草案、仅权利要求、技术交底分析和 paper-patent comparison。
- 处理可选中文本 PDF、扫描 PDF、粘贴文本和混合项目文件夹。
- 将算法/软件、装置/系统、工艺/材料和混合型发明路由到不同起草规则。
- 保留来源支持的核心公式,并在 DOCX 中渲染为可编辑 Office Math。
- 生成与权利要求对齐的黑白流程图 SVG 和 PNG。
- 复用主权利要求流程图作为摘要附图和说明书附图。
- 分别生成中文 DOCX:权利要求书、说明书、说明书摘要、摘要附图和完整审阅稿。
- 校验权利要求结构、证据可追溯性、公式覆盖、附图对齐、术语一致性和质量阈值。
文件结构
nature-paper-to-patent/
├── README.md
├── SKILL.md
├── manifest.yaml
├── requirements.txt
├── static/
│ ├── core/
│ │ ├── principles.md
│ │ ├── workflow.md
│ │ └── output-contract.md
│ └── fragments/
│ ├── source/
│ ├── task/
│ └── invention/
├── references/
│ ├── cn-patent-drafting-guide.md
│ ├── corpus-derived-patterns.md
│ ├── corpus-pair-audit.md
│ ├── draft-schema.md
│ └── patent-figure-guide.md
├── scripts/
│ ├── audit_claims.py
│ ├── build_patent_package.py
│ ├── extract_pdf_text.py
│ ├── init_patent_project.py
│ ├── math_to_omml.py
│ ├── render_flowchart_svg.py
│ ├── render_patent_docx.py
│ └── validate_patent_draft.py
├── evals/
│ └── evals.json
└── tests/
└── test_validation.py路由模型
短 SKILL.md 作为路由器。manifest.yaml 只选择当前请求需要的片段:
source_format:pdf-text、scanned-pdf、pasted-text或mixed-projecttask_mode:full-draft、claim-set、disclosure-analysis或paper-patent-auditinvention_type:algorithm-software、apparatus-system、process-material或mixed
始终加载的 core 定义证据纪律、分阶段起草工作流和输出契约。
默认工作流
1. 记录输入、发表状态、发明人问题和权属问题。 2. 建立全文 source map。 3. 建立术语、公式、图表和输入-操作-输出清单。 4. 建立 evidence ledger。 5. 形成发明构思和权利要求策略。 6. 起草并审查权利要求。 7. 对齐说明书、公式、附图、实施例和摘要。 8. 校验并生成完整申请文件包。
每个阶段都有明确 gate。无支撑特征会被排除在正式权利要求之外,未解决事实会保留为发明人问题,而不会被编造。
安装
安装完整目录,而不是只安装 SKILL.md,因为路由器依赖 manifest.yaml、static/、references/ 和 scripts/。
安装依赖:
python -m pip install -r requirements.txt运行自动检查:
python -m unittest discover -s tests -v示例请求
请读取并遵循 nature-paper-to-patent 技能。
分析 paper/paper.pdf,并生成中国发明专利草稿。
请分别创建中文权利要求书、说明书、说明书摘要和摘要附图 DOCX 文件。
保留来源支持的核心公式,并以可编辑 Office Math 呈现;
生成与权利要求对齐的主流程图和方法附图;
将每个实质性权利要求特征映射到来源证据。默认交付物
outputs/
├── patent-权利要求书.docx
├── patent-说明书.docx
├── patent-说明书摘要.docx
├── patent-摘要附图.docx
├── patent-完整审阅稿.docx
├── patent-结构化草稿.json
├── patent-权利要求检查.txt
├── patent-草稿验证报告.txt
└── patent-figures/
├── figure-1.svg
└── figure-1.png状态与边界
状态:Beta。
确定性校验脚本和合成测试覆盖权利要求映射、核心公式要求、附图生成、可编辑 Office Math 和 DOCX 打包。输出仍是起草辅助材料,不构成专利性、新颖性、发明人资格、权属、侵权或可提交性意见;正式使用前需要发明人确认,并由合格中国专利专业人员审阅。
贡献者:`snipp-zha`。
Chinese Invention Patent Drafting Guide
Contents
1. Evidence discipline 2. Converting paper structure 3. Independent claims 4. Dependent claims 5. Specification 6. Algorithm-related inventions 7. Final audit 8. Quality rubric
1. Evidence Discipline
Create the claim language from disclosed operations, relationships, structures, and parameters. A paper's broad statement of purpose is not, by itself, support for every implementation that could achieve that purpose.
Distinguish:
- a result demonstrated by experiments;
- a technical effect caused by identified features;
- an aspiration or future research direction.
Use the first two with appropriate scope. Exclude the third unless inventors provide additional disclosure.
2. Converting Paper Structure
Map common paper sections as follows:
| Paper material | Patent destination |
|---|---|
| research motivation and limitations | background and technical problem |
| contribution list | candidate essential features and dependent-claim branches |
| method overview | independent-claim sequence |
| module architecture | dependent claims and embodiments |
| formulas and losses | dependent claims and detailed embodiments |
| dataset preparation | acquisition/preprocessing claims and embodiments |
| experiments and ablations | beneficial effects and verification examples |
| conclusion | effect summary, not a substitute for technical detail |
| limitations/future work | inventor questions; normally not claimed |
Rewrite causal and operational relationships. Do not merely translate academic prose.
3. Independent Claims
For a method claim:
- identify the technical input;
- state the essential processing sequence;
- preserve dependencies between intermediate data;
- state the specific domain result or control action, such as a detection result, estimated state, target position, classification result, or control instruction;
- include only features needed for the central effect.
Prefer observable operations over labels such as "intelligent module" or "novel network." Define what the module receives, performs, and produces.
Avoid:
- result-only wording unsupported by means;
- unnecessary dataset names, exact model depth, or experimental values;
- optional features mixed into the essential chain;
- steps that appear only in the patent draft and not in the source.
4. Dependent Claims
Use dependent claims to build fallback positions around:
- data sources, labels, and preprocessing;
- component topology and data flow;
- feature extraction branches;
- training stages and loss functions;
- reconstruction, fusion, attention, or optimization operations;
- inference and post-processing;
- equations, thresholds, ranges, and preferred parameters;
- deployment as device and storage medium.
Each dependent claim must add a technical limitation. A statement of advantage alone does not narrow a claim.
5. Specification
Technical Field
Use one concise paragraph naming the relevant technical field and the specific subject.
Background
Describe known approaches and a concrete technical deficiency without unsupported admissions about the closest prior art. Do not use the paper's own method as background.
Invention Content
State:
1. the technical problem; 2. the technical solution in language aligned with the claims; 3. beneficial effects linked to particular features.
Figures
Propose only figures supported by available information, such as:
- overall method flow;
- model or system architecture;
- core module structure;
- training and inference flow;
- data-processing flow.
Mark missing drawings as [待补图].
Detailed Embodiments
Provide enough operational detail for implementation. Include data origin, preprocessing, model flow, training, inference, formulas, parameter examples, and evaluation where disclosed.
Present paper-specific settings as examples unless they are essential. Preserve alternatives disclosed in the source to support broader scope.
Formulas
For a paper whose technical contribution depends on mathematical operations, reproduce the core formulas in patent notation and explain them in the specification.
Include formulas that define:
- sample selection or prototype computation;
- feature transformation or reconstruction;
- attention, weighting, alignment, or fusion;
- training objectives and loss functions;
- estimation, detection, positioning, or decision rules.
For every formula:
1. assign a consecutive patent formula number; 2. identify its source page and paper formula number in the drafting record; 3. define every variable, index, operator, norm, and weight; 4. explain what input the formula processes and what output it produces; 5. connect the formula to a method step and technical effect.
Do not leave a core operation described only as "calculated according to a formula." Do not add unrelated evaluation metrics merely to increase the number of formulas.
Abstract
Summarize the field, problem, essential solution, and main technical effect. Keep terminology aligned with claim 1 and avoid promotional language.
6. Algorithm-Related Inventions
Tie algorithmic operations to a technical context and technical data, such as sensor signals, industrial process variables, images, battery measurements, or remote-sensing data.
Describe:
- how data is obtained;
- how data is transformed;
- how model components cooperate;
- what technical quantity, state, location, class, or control instruction is produced;
- how that output improves a technical process.
Do not rely solely on accuracy claims. Explain the mechanism that reduces redundancy, preserves multiscale information, handles domain shift, reconstructs disturbed features, or otherwise produces the effect.
7. Final Audit
Confirm:
- claim 1 can be traced line by line to the source;
- dependent claims have correct antecedent basis;
- all claimed terms appear in the specification;
- the same object is not given multiple names;
- no equation has undefined variables;
- every source-supported core formula is present in the specification rather than only in claims or notes;
- the flowchart exists as both SVG and PNG and the PNG is embedded in the specification;
- the designated abstract figure is the same main figure reused in the specification and is embedded in the abstract deliverables;
- claims, specification, and abstract are delivered as separate DOCX files;
- no performance number is copied without its test context;
- publication and filing dates are flagged for professional novelty review;
- the draft is labeled for inventor and patent-professional review.
8. Quality Rubric
Score each dimension from 1 to 5 and give one sentence of evidence for the score.
| Dimension | 1 | 3 | 5 |
|---|---|---|---|
| evidence support | major claimed features are unsupported | most features are traceable, with gaps marked | every claimed feature is traceable and unsupported matter is excluded |
| claim architecture | scope is incoherent or dependencies fail | usable independent claim with limited fallback positions | clear essential chain and layered fallback positions across appropriate categories |
| terminology and consistency | conflicting terms and broken references | mostly consistent with minor issues | terms, symbols, dependencies, figures, and sections align throughout |
| enablement detail | result-only description | principal implementation is described | data, operations, relationships, parameters, and alternatives are sufficiently described |
| technical-effect reasoning | effects are promotional or detached | some effects are linked to features | each material effect is causally tied to disclosed technical means |
Delivery thresholds:
- Require at least 4 for evidence support.
- Require at least 4 for claim architecture.
- Require at least 4 for terminology and consistency.
- Require at least 3 for enablement detail.
- Require no unresolved structural errors from
scripts/audit_claims.py.
If a threshold is missed, label the output incomplete draft and list the exact inventor input needed to improve it.
Corpus-Derived Paper-to-Patent Patterns
Scope
These heuristics were derived from five paired research-paper and Chinese-patent examples covering robotic grasping, battery health estimation, industrial fault detection, surface defect detection, and remote-sensing biomass estimation.
They describe recurring drafting choices. They do not establish patentability or legal sufficiency.
Recurring Transformations
| Academic contribution | Typical patent treatment |
|---|---|
| end-to-end model | method claim with ordered data and model operations |
| new backbone or feature extractor | dependent claim defining stages, branches, or topology |
| attention, fusion, or reconstruction module | dependent claims defining input, operation, and output |
| training strategy | method steps plus loss-function dependent claims |
| inference rule | post-processing or decision step |
| dataset extension | data acquisition, annotation, or preprocessing dependent claim |
| experimental advantage | beneficial effect tied to the responsible features |
| software implementation | device and computer-readable-medium claims |
Claim-Layer Pattern
The examples commonly use:
1. one method claim covering the complete technical pipeline; 2. dependent claims unpacking each contribution in the same order as the pipeline; 3. dependent claims for formulas or parameterized operations; 4. device and storage-medium claims for software-controlled implementations.
Do not copy this shape blindly. Use it only where supported by the source and appropriate for the invention.
Problem-Solution Patterns
Multi-task Interference
When a paper addresses interference between tasks, claim the cooperating feature paths or reconstruction operations rather than only stating that interference is reduced.
Domain Shift and Generalization
When a paper addresses multiple operating conditions, claim domain construction, meta-training/meta-testing or adaptation operations, feature fusion, and model updating. Treat generalization as an effect, not an unsupported step.
Redundant Latent Features
When orthogonality or introspective training is used, separate the detection pipeline from training-specific dependent claims. Define positive/negative samples and loss terms before relying on their effects.
Detection Accuracy and Speed
For multiscale detection, claim the feature hierarchy, candidate-region generation, and localization operations. Place exact backbone depth, anchor parameters, and benchmark values in narrower claims or embodiments.
Limited Samples
For self-supervised reconstruction, claim how auxiliary/disturbed data is generated, how residual data is formed, how reconstruction pretraining works, and how the downstream estimator is fine-tuned.
Drafting Lessons
- Papers often emphasize novelty by naming modules; patents must disclose the module's operations and relationships.
- The paper's contribution list is a strong candidate list for dependent claims, not necessarily for separate independent claims.
- Experimental tables support effect statements but usually should not define claim scope.
- Model names can remain in embodiments, but independent claims should generally use technically descriptive language.
- A paper and its patent may differ in terminology or implementation detail. Treat the paper as evidence, not permission to import unexplained features from another patent.
- Publication timing, prior disclosures, and inventorship require separate factual and professional review.
Corpus Pair Audit
Purpose
Use the five numbered pairs as examples of drafting transformations, not as unquestioned one-to-one ground truth. File numbering establishes intended pairing only. Technical content and dates determine the strength of each match.
Pair Assessments
| Pair | Paper subject | Patent subject | Match strength | Important caution |
|---|---|---|---|---|
| 1 | MLEAFormer for grasp detection and classification | ECBAFormer robotic grasp detection method | partial | The task and broad architecture align, but model and module names differ. Verify every feature before transferring it. |
| 2 | MSDMLN for battery SOH under multiple target conditions | battery SOH estimator training using EMD, dense recurrent-convolution extraction, and meta-learning | strong | The patent application predates the paper publication. Use the technical overlap, but do not infer legal priority or inventorship. |
| 3 | ReSiOrCAE with variable rearrangement, dual-path convolution, orthogonality, and soft-introspective training | introspective orthogonal autoencoder process fault detection | partial | The patent strongly covers introspective and orthogonal training but does not necessarily capture every later paper contribution. |
| 4 | AGLNet for adaptive global localization surface-defect detection | AGLNet product surface-defect detection | strong | The patent mirrors the main pipeline closely. Exact legal status and chronology still require source records. |
| 5 | SSDFRN for limited-data mangrove biomass estimation | self-supervised disturbing-feature reconstruction biomass estimation | strong | The core data disturbance, reconstruction, multiview network, and fine-tuning chain align well. |
Date Cautions
- A publication date is not necessarily the first public disclosure date.
- A patent PDF may show application, publication, or grant dates; distinguish them.
- A paper may have received, accepted, online-publication, and issue dates.
- Never state novelty, priority, ownership, or valid entitlement from these PDFs alone.
Pair-Mapping Procedure
For each pair:
1. Normalize technical terms without collapsing distinct modules. 2. Map the independent claim feature by feature to paper pages. 3. Map dependent claims to paper methods, formulas, and implementation details. 4. Label patent-only material and exclude it when drafting solely from the paper. 5. Label paper-only material as a candidate contribution, not as proof of patentability. 6. Record chronology separately from technical similarity.
Safe Uses
Use the corpus to learn:
- how contribution lists become claim branches;
- how model operations become ordered method steps;
- how experiments become effect evidence;
- how software methods expand to device and storage-medium claims.
Do not use it to:
- copy claim language into an unrelated invention;
- assume patent-only detail is supported by the paper;
- determine novelty or freedom to operate;
- infer inventors from paper authors.
Structured Draft Schema
Purpose
Populate a UTF-8 JSON file with this structure before rendering a DOCX. Empty optional arrays are allowed. Preserve [TO CONFIRM: ...] markers where facts remain unresolved.
{
"schema_version": "2.0",
"title": "一种……方法、设备及介质",
"metadata": {
"source": "paper.pdf",
"target": "中国发明专利",
"draft_status": "供发明人及专利代理师复核"
},
"source_analysis": {
"contains_core_formulas": true,
"formula_count_in_source": 18,
"contains_methodology_figures": true
},
"source_map": [
{
"id": "P001",
"type": "paper-text",
"locator": "第3页,2.2节,第1段",
"summary": "公开核心特征提取流程",
"confidence": "high"
},
{
"id": "E001",
"type": "equation",
"locator": "第4页,公式(1)",
"summary": "类别原型计算",
"confidence": "high"
}
],
"terminology_ledger": [
{
"concept": "类别原型",
"canonical_zh": "类别原型",
"source_terms": ["class prototype", "prototype"],
"forbidden_aliases": ["类别中心"]
}
],
"formula_inventory": [
{
"source_id": "E001",
"source_number": "(1)",
"technical_role": "根据支持集特征计算类别原型",
"disposition": "specification-equation-1"
}
],
"figure_inventory": [
{
"source_id": "F001",
"source_number": "Fig. 2",
"type": "methodology",
"disposition": "redraw-as-figure-2"
}
],
"abstract_figure_number": 1,
"assumptions": [
"目标法域为中国"
],
"invention_concept": {
"technical_problem": "……",
"technical_means": "……",
"technical_effect": "……"
},
"evidence_ledger": [
{
"id": "F1",
"feature": "……",
"source_ids": ["P001", "E001"],
"source_location": "第3页,2.2节",
"technical_role": "……",
"effect": "……",
"support_status": "explicit"
}
],
"claims": [
{
"number": 1,
"text": "一种……方法,其特征在于,包括:……"
},
{
"number": 2,
"text": "根据权利要求1所述的方法,其特征在于,……"
}
],
"claim_feature_map": [
{
"claim_number": 1,
"feature": "根据支持集特征计算类别原型",
"evidence_ids": ["F1"],
"specification_locations": ["具体实施方式,实施例1"]
}
],
"figures": [
{
"number": 1,
"title": "方法流程图",
"type": "flowchart",
"orientation": "vertical",
"claim_number": 1,
"complete_claim_flow": true,
"source_ids": ["P001"],
"nodes": [
{
"id": "S1",
"label": "S1:获取并预处理待检测数据",
"claim_step": "S1"
},
{
"id": "S2",
"label": "S2:提取多尺度特征",
"claim_step": "S2"
}
],
"edges": [
{
"from": "S1",
"to": "S2",
"label": ""
}
]
},
{
"number": 2,
"title": "核心方法结构示意图",
"type": "methodology",
"orientation": "horizontal",
"source_ids": ["F001", "P001"],
"nodes": [
{
"id": "input",
"label": "输入特征"
},
{
"id": "module",
"label": "核心处理模块"
},
{
"id": "output",
"label": "输出特征"
}
],
"edges": [
{
"from": "input",
"to": "module",
"label": ""
},
{
"from": "module",
"to": "output",
"label": ""
}
]
}
],
"specification": {
"technical_field": [
"本发明涉及……"
],
"background": [
"……"
],
"invention_content": {
"problem": [
"……"
],
"solution": [
"……"
],
"beneficial_effects": [
"……"
]
},
"figure_descriptions": [
"图1为本发明方法流程图。"
],
"equations": [
{
"number": 1,
"source_location": "论文第4页,公式(1)",
"source_ids": ["E001"],
"expression": "O_u = (1/|S_u|) Σ_(x_i,y_i∈S_u) h_γ(x_i)",
"latex": "O_u = \\frac{1}{|S_u|}\\sum_{(x_i,y_i)\\in S_u} h_\\gamma(x_i)",
"symbols": [
{"symbol": "O_u", "meaning": "类别u的类别原型"},
{"symbol": "S_u", "meaning": "类别u的支持集"},
{"symbol": "h_\\gamma", "meaning": "监督特征提取器"}
],
"technical_role": "对同一类别样本的监督特征求均值以获得类别原型",
"description": "其中,O_u表示类别u的类别原型,S_u表示类别u的支持集,h_γ表示监督特征提取器。该公式通过对同一类别样本的监督特征求均值获得类别原型。"
}
],
"embodiments": [
{
"heading": "实施例1",
"paragraphs": [
"……"
]
}
]
},
"abstract": "本发明涉及……",
"audit": {
"support_findings": [
"……"
],
"consistency_findings": [
"……"
]
},
"quality_assessment": {
"status": "review-draft",
"scores": {
"evidence_support": {"score": 4, "evidence": "每项权利要求特征均映射到证据台账。"},
"claim_architecture": {"score": 4, "evidence": "独立权利要求形成完整技术链,并设置从属回退层。"},
"terminology_consistency": {"score": 4, "evidence": "权利要求、说明书和附图使用统一术语。"},
"enablement_detail": {"score": 3, "evidence": "已说明主要数据流、公式和实施步骤。"},
"technical_effect_reasoning": {"score": 3, "evidence": "主要效果已关联到对应技术手段。"},
"formula_coverage": {"score": 4, "evidence": "核心公式均已收录并定义符号。"},
"figure_alignment": {"score": 4, "evidence": "主流程图与权利要求1步骤一致。"}
}
},
"inventor_questions": [
"[TO CONFIRM: ……]"
]
}Rules
- Use integer claim numbers in ascending order.
- Use stable source IDs:
Pfor paper text,Efor equations,Ffor
source figures, and C for code or supplementary evidence.
- Give every
explicitorinherentevidence-ledger item one or more
source_ids.
- Add at least one
claim_feature_mapentry for every formal claim. Map each
material limitation to evidence-ledger IDs, not merely to a general page.
- Store claim text without repeating the number at its beginning.
- Use arrays for paragraphs to preserve paragraph boundaries.
- Use only
explicit,inherent,needs-confirmation, orunsupportedas evidence status. - Exclude unsupported features from formal claims.
- Keep internal audit material in the appendix, not in the formal application sections.
- Number figures consecutively from 1.
- Set
abstract_figure_numberto the main figure used as the abstract figure. - Reuse that exact figure in the specification; do not create a conflicting duplicate.
- The abstract figure should normally be an overall method or system flow that represents the principal independent claim.
- Use
flowchartas the figure type andverticalorhorizontalas the orientation. - Use
methodologyfor an intermediate architecture, module, feature-flow, or loss-relationship figure. - A methodology figure does not require
claim_number,claim_step, orcomplete_claim_flow. - Prefer a paper figure when suitable; otherwise redraw it as a concise black-and-white patent figure.
- Give every node a unique ASCII identifier.
- Keep
claim_stepequal to the corresponding identifier in the method claim, such asS1. - Set
claim_numberto the method claim represented by the figure. - Set
complete_claim_flowtotruefor an overall flowchart that must cover every numbered step in that claim. - Reference every figure in
specification.figure_descriptions. - Use concise node labels; put implementation detail in the specification.
- If the paper contains formulas that define core technical operations, populate
specification.equations. - Set
source_analysis.contains_core_formulasafter reviewing the paper. - Number equations consecutively from 1.
- Add a valid
latexfield to every equation; the renderer converts it to editable Office Math. - Add
source_ids, structuredsymbols, andtechnical_roleto every
equation.
- Treat
expressionas a readable audit copy, not as the DOCX rendering source. - Record the paper page and original formula number in
source_location. - Define every symbol in
descriptionand state the technical operation performed by the formula. - Include formulas in the standalone specification DOCX; do not place them only in an internal appendix.
- Record a disposition for every core source formula and methodology figure.
- Populate
quality_assessment.scoreswith a 1-5 score and evidence sentence
for each dimension required by static/core/output-contract.md.
Patent Figure Guide
Method Flowcharts
Build the main method flowchart from the ordered operations in the independent method claim.
Use:
- black strokes and white fills;
- rectangular process nodes;
- solid directional arrows;
- figure numbers such as
图1; - step identifiers such as
S1,S2, andS3; - concise Chinese operation labels.
Write the final node as the concrete result of the method. Examples include:
输出缺陷检测结果;获得故障检测结果;输出目标探测结果;获得电池健康状态估计结果;输出目标类别和目标位置.
Keep the result name identical to the independent claim. Do not use invented umbrella terms such as 技术结果.
Abstract Figure
Use the main overall method flowchart as the abstract figure unless another single figure better represents the principal technical solution.
The same figure may and normally should also appear in the specification as 图1. Reuse the same drawing file, figure number, node labels, and arrows. Do not generate separate "abstract" and "specification" versions that differ in wording or flow.
The abstract figure must:
- represent the principal independent claim;
- show the main input, core operations, and specific output;
- remain readable without equations or experimental details;
- be generated as both SVG and PNG;
- be embedded in the abstract-figure DOCX, abstract DOCX, and specification DOCX.
Avoid:
- color as the only carrier of meaning;
- gradients, shadows, decorative icons, or photographic backgrounds;
- unsupported branches or modules;
- effect-only nodes such as "提高准确率";
- vague final nodes such as
输出技术结果,获得处理结果, or输出最终结果; - dense equations or experimental results inside nodes;
- inconsistent terminology between the figure and claims.
Consistency Rules
Confirm:
1. every claim_step appears in a method claim; 2. an overall figure marked complete_claim_flow covers every numbered step of its referenced claim; 3. node order follows the claimed data flow; 4. every edge connects existing nodes; 5. every node is reachable in the intended flow; 6. the figure description uses the same figure number and title; 7. the embodiment explains each node's operation; 8. optional details remain outside the main flow unless they form a disclosed branch. 9. abstract_figure_number points to an existing complete main figure; 10. the abstract and specification reuse the same image file.
Figure Set
For algorithm-related inventions, consider:
- overall method flowchart;
- system or model architecture;
- core module structure;
- training flow;
- inference flow;
- data preprocessing flow.
Generate only figures supported by the source. The bundled script currently renders deterministic method flowcharts. Describe unsupported figure types as [TO CONFIRM: figure required] rather than fabricating them.
latex2mathml>=3.77.0
Pillow>=10.0.0
pypdf>=4.0.0
python-docx>=1.1.0
#!/usr/bin/env python3
"""Run deterministic structural checks on Chinese patent claims."""
import argparse
import json
import re
from dataclasses import dataclass
from pathlib import Path
CLAIM_START = re.compile(r"(?m)^\s*(\d+)\s*[.、.]\s*")
REFERENCE = re.compile(
r"权利要求\s*(\d+)(?:\s*[-—~~至]\s*(\d+))?"
r"|权利要求\s*(\d+)\s*(?:或|、)\s*(\d+)"
)
TERM_INTRO = re.compile(r"(?:所述|该)([\u4e00-\u9fffA-Za-z][\u4e00-\u9fffA-Za-z0-9_-]{1,20})")
PLACEHOLDER = re.compile(r"\[(?:TO CONFIRM|待确认)[^\]]*\]", re.IGNORECASE)
@dataclass
class Finding:
level: str
claim: int | None
code: str
message: str
def split_claims(text: str) -> list[tuple[int, str]]:
matches = list(CLAIM_START.finditer(text))
claims = []
for index, match in enumerate(matches):
end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
claims.append((int(match.group(1)), text[match.end() : end].strip()))
return claims
def references(body: str) -> list[int]:
result = []
for match in REFERENCE.finditer(body):
if match.group(1):
start = int(match.group(1))
finish = int(match.group(2) or start)
result.extend(range(start, finish + 1))
else:
result.extend((int(match.group(3)), int(match.group(4))))
return sorted(set(result))
def normalize(text: str) -> str:
return re.sub(r"\s+", "", text)
def audit(text: str) -> list[Finding]:
claims = split_claims(text)
findings = []
if not claims:
return [Finding("ERROR", None, "NO_CLAIMS", "未识别到以“1.”形式起始的权利要求。")]
numbers = [number for number, _ in claims]
expected = list(range(1, len(claims) + 1))
if numbers != expected:
findings.append(
Finding("ERROR", None, "NUMBER_SEQUENCE", f"编号应连续为{expected},实际为{numbers}。")
)
previous_text = ""
claim_map = {}
for number, body in claims:
compact = normalize(body)
claim_map[number] = compact
refs = references(body)
if not body:
findings.append(Finding("ERROR", number, "EMPTY", "权利要求正文为空。"))
continue
if PLACEHOLDER.search(body):
findings.append(
Finding("ERROR", number, "PLACEHOLDER", "正式权利要求中仍含待确认标记。")
)
if number == 1 and refs:
findings.append(
Finding("ERROR", number, "INDEPENDENT_REFERENCE", "权利要求1不应引用其他权利要求。")
)
if number > 1 and not refs:
findings.append(
Finding("WARNING", number, "NO_REFERENCE", "未检测到从属引用;确认其是否为独立权利要求。")
)
for ref in refs:
if ref >= number:
findings.append(
Finding("ERROR", number, "FORWARD_REFERENCE", f"引用了非在先权利要求{ref}。")
)
if ref not in claim_map:
findings.append(
Finding("ERROR", number, "MISSING_REFERENCE", f"引用的权利要求{ref}不存在。")
)
if "其特征在于" not in compact:
findings.append(
Finding("WARNING", number, "TRANSITION", "未检测到“其特征在于”过渡语。")
)
if len(compact) < 25:
findings.append(
Finding("WARNING", number, "TOO_SHORT", "权利要求较短,确认是否完整限定技术方案。")
)
if re.search(r"(效果更好|性能优异|显著提高|大大提高|最佳|最优)", compact):
findings.append(
Finding("WARNING", number, "RESULT_LANGUAGE", "含结果或宣传性措辞,确认是否改为技术限定。")
)
searchable_basis = previous_text + "".join(
claim_map.get(ref, "") for ref in refs
)
for term in sorted(set(TERM_INTRO.findall(body))):
if term in {"方法", "装置", "设备", "系统", "步骤", "程序"}:
continue
if term not in searchable_basis and compact.find(term) <= 4:
findings.append(
Finding(
"WARNING",
number,
"ANTECEDENT_BASIS",
f"术语“{term}”可能缺少清晰的前置基础。",
)
)
previous_text += compact
return findings
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("claims", type=Path, help="UTF-8 claims text file")
parser.add_argument("--json", action="store_true", help="Output findings as JSON")
args = parser.parse_args()
text = args.claims.read_text(encoding="utf-8")
findings = audit(text)
if args.json:
print(json.dumps([finding.__dict__ for finding in findings], ensure_ascii=False, indent=2))
elif not findings:
print("PASS: 未发现结构性问题。")
else:
for finding in findings:
location = f"权利要求{finding.claim}" if finding.claim else "整体"
print(f"{finding.level}\t{location}\t{finding.code}\t{finding.message}")
errors = sum(finding.level == "ERROR" for finding in findings)
warnings = sum(finding.level == "WARNING" for finding in findings)
print(f"\n汇总: {errors} 个错误, {warnings} 个警告")
return 1 if any(finding.level == "ERROR" for finding in findings) else 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Build the standard split Chinese patent application package."""
import argparse
import importlib.util
import json
import shutil
import subprocess
import sys
from pathlib import Path
def run(command: list[str]) -> None:
subprocess.run(command, check=True)
def validate(data: dict) -> None:
required = ("title", "claims", "specification", "abstract", "figures")
missing = [key for key in required if not data.get(key)]
if missing:
raise ValueError(f"Missing required complete-package content: {', '.join(missing)}")
spec = data["specification"]
if not spec.get("figure_descriptions"):
raise ValueError("Specification must contain figure descriptions")
if "equations" not in spec:
raise ValueError(
"Specification must contain an equations array; use an empty array only when "
"the source contains no core technical formulas"
)
source_analysis = data.get("source_analysis", {})
if source_analysis.get("contains_core_formulas") and not spec.get("equations"):
raise ValueError(
"The source is marked as containing core formulas, but specification.equations is empty"
)
for equation in spec.get("equations", []):
if not equation.get("latex"):
raise ValueError(
f"Equation {equation.get('number')} must include latex source for native Office Math"
)
equation_numbers = [item.get("number") for item in spec.get("equations", [])]
if equation_numbers and equation_numbers != list(range(1, len(equation_numbers) + 1)):
raise ValueError(
f"Equation numbers must be consecutive integers starting at 1: {equation_numbers}"
)
abstract_figure_number = data.get("abstract_figure_number")
if not isinstance(abstract_figure_number, int):
raise ValueError("abstract_figure_number must be an integer")
figure = next(
(
item
for item in data["figures"]
if item.get("number") == abstract_figure_number
),
None,
)
if figure is None:
raise ValueError(
f"abstract_figure_number {abstract_figure_number} does not reference an existing figure"
)
if not figure.get("complete_claim_flow"):
raise ValueError("The abstract figure must be a complete principal claim flow")
if source_analysis.get("contains_methodology_figures"):
methodology = [item for item in data["figures"] if item.get("type") == "methodology"]
if not methodology:
raise ValueError(
"The source is marked as containing methodology figures, but no methodology "
"figure is included"
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("draft", type=Path, help="UTF-8 patent draft JSON")
parser.add_argument("--output-dir", type=Path, required=True)
parser.add_argument("--prefix", default="patent")
args = parser.parse_args()
data = json.loads(args.draft.read_text(encoding="utf-8"))
root = Path(__file__).resolve().parent
validator_script = root / "validate_patent_draft.py"
validator_spec = importlib.util.spec_from_file_location(
"patent_draft_validator", validator_script
)
validator = importlib.util.module_from_spec(validator_spec)
sys.modules[validator_spec.name] = validator
validator_spec.loader.exec_module(validator)
validation_findings = validator.validate(data)
args.output_dir.mkdir(parents=True, exist_ok=True)
validation_report = args.output_dir / f"{args.prefix}-草稿验证报告.txt"
validation_report.write_text(
validator.format_report(validation_findings), encoding="utf-8"
)
if any(item.level == "ERROR" for item in validation_findings):
print(validation_report)
raise SystemExit(1)
validate(data)
docx_script = root / "render_patent_docx.py"
figure_script = root / "render_flowchart_svg.py"
figure_dir = args.output_dir / f"{args.prefix}-figures"
run(
[
sys.executable,
str(figure_script),
str(args.draft),
"--output-dir",
str(figure_dir),
"--png",
]
)
outputs = {
"claims": args.output_dir / f"{args.prefix}-权利要求书.docx",
"specification": args.output_dir / f"{args.prefix}-说明书.docx",
"abstract": args.output_dir / f"{args.prefix}-说明书摘要.docx",
"abstract-figure": args.output_dir / f"{args.prefix}-摘要附图.docx",
"all": args.output_dir / f"{args.prefix}-完整审阅稿.docx",
}
for part, output in outputs.items():
command = [
sys.executable,
str(docx_script),
str(args.draft),
"--output",
str(output),
"--part",
part,
]
if part in {"specification", "abstract", "abstract-figure", "all"}:
command.extend(["--figure-dir", str(figure_dir)])
run(command)
claims_text = args.output_dir / f"{args.prefix}-权利要求书.txt"
claims_text.write_text(
"\n".join(f"{claim['number']}. {claim['text']}" for claim in data["claims"]) + "\n",
encoding="utf-8",
)
audit = args.output_dir / f"{args.prefix}-权利要求检查.txt"
audit_script = root / "audit_claims.py"
spec = importlib.util.spec_from_file_location("patent_claim_audit", audit_script)
audit_module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = audit_module
spec.loader.exec_module(audit_module)
findings = audit_module.audit(claims_text.read_text(encoding="utf-8"))
if findings:
lines = []
for finding in findings:
location = f"权利要求{finding.claim}" if finding.claim else "整体"
lines.append(
f"{finding.level}\t{location}\t{finding.code}\t{finding.message}"
)
else:
lines = ["PASS: 未发现权利要求结构性问题。"]
audit.write_text("\n".join(lines) + "\n", encoding="utf-8")
if any(finding.level == "ERROR" for finding in findings):
raise SystemExit(1)
json_copy = args.output_dir / f"{args.prefix}-结构化草稿.json"
if args.draft.resolve() != json_copy.resolve():
shutil.copy2(args.draft, json_copy)
for output in (*outputs.values(), json_copy, audit, validation_report):
print(output)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Extract searchable text from one PDF or a directory of PDFs."""
import argparse
from pathlib import Path
from pypdf import PdfReader
def extract(source: Path, destination: Path) -> tuple[int, int]:
reader = PdfReader(source)
pages = []
for number, page in enumerate(reader.pages, start=1):
text = page.extract_text() or ""
pages.append(f"\n\n===== PAGE {number} =====\n\n{text}")
content = "".join(pages)
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(content, encoding="utf-8")
return len(reader.pages), len(content.strip())
def collect_pdfs(source: Path) -> list[Path]:
if source.is_file():
if source.suffix.lower() != ".pdf":
raise ValueError(f"Input is not a PDF: {source}")
return [source]
if source.is_dir():
return sorted(path for path in source.rglob("*.pdf") if path.is_file())
raise FileNotFoundError(f"Input does not exist: {source}")
def output_path(pdf: Path, source: Path, output: Path) -> Path:
if source.is_file():
if output.suffix.lower() == ".txt":
return output
return output / f"{pdf.stem}.txt"
relative = pdf.relative_to(source).with_suffix(".txt")
return output / relative
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source", type=Path, help="PDF file or directory")
parser.add_argument(
"--output",
type=Path,
required=True,
help="Output text file or directory",
)
args = parser.parse_args()
try:
pdfs = collect_pdfs(args.source)
except (FileNotFoundError, ValueError) as error:
parser.error(str(error))
if not pdfs:
parser.error(f"No PDF files found under: {args.source}")
low_text = []
for pdf in pdfs:
destination = output_path(pdf, args.source, args.output)
pages, characters = extract(pdf, destination)
print(f"{pdf}\t{pages} pages\t{characters} characters\t{destination}")
if characters < max(200, pages * 50):
low_text.append(pdf)
if low_text:
print("\nOCR may be required for:")
for pdf in low_text:
print(f"- {pdf}")
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Create an agent-neutral paper-to-patent project workspace."""
import argparse
import json
import shutil
from pathlib import Path
DIRECTORIES = (
"paper",
"supplementary/source-code",
"source-figures",
"existing-patent",
"work",
"outputs",
)
SKILL_FILES = ("SKILL.md", "manifest.yaml", "requirements.txt")
SKILL_DIRECTORIES = ("static", "references", "scripts")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("project_dir", type=Path)
parser.add_argument("--paper", type=Path, help="Optional paper to copy into paper/")
parser.add_argument("--force", action="store_true", help="Allow an existing project directory")
parser.add_argument(
"--no-embed-skill",
action="store_true",
help="Create only case directories without copying the skill bundle",
)
args = parser.parse_args()
project = args.project_dir.resolve()
if project.exists() and any(project.iterdir()) and not args.force:
parser.error("project directory is not empty; use --force to add missing structure")
project.mkdir(parents=True, exist_ok=True)
for directory in DIRECTORIES:
(project / directory).mkdir(parents=True, exist_ok=True)
if not args.no_embed_skill:
skill_root = Path(__file__).resolve().parents[1]
if project != skill_root:
for filename in SKILL_FILES:
shutil.copy2(skill_root / filename, project / filename)
for directory in SKILL_DIRECTORIES:
shutil.copytree(
skill_root / directory,
project / directory,
dirs_exist_ok=True,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
)
if args.paper:
paper = args.paper.resolve()
if not paper.is_file():
parser.error(f"paper does not exist: {paper}")
shutil.copy2(paper, project / "paper" / paper.name)
intake = {
"target_jurisdiction": "中国发明专利",
"requested_deliverable": "full-draft",
"publication_status": "[TO CONFIRM: 论文是否已经公开]",
"publication_dates": [],
"inventorship": "[TO CONFIRM: 按实际技术贡献确认发明人]",
"ownership": "[TO CONFIRM: 确认申请人和权属]",
"source_files": [],
}
intake_path = project / "work" / "00-intake.json"
if not intake_path.exists():
intake_path.write_text(
json.dumps(intake, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
notes = project / "supplementary" / "inventor-notes.md"
if not notes.exists():
notes.write_text(
"# 发明人补充说明\n\n"
"- 实际技术贡献:\n"
"- 与论文不同的工程实现:\n"
"- 可替代方案和参数范围:\n"
"- 首次公开时间与方式:\n"
"- 希望重点保护的内容:\n",
encoding="utf-8",
)
print(project)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Convert a LaTeX equation into editable Word Office Math (OMML)."""
from copy import deepcopy
from xml.etree import ElementTree
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
MATHML_NS = "{http://www.w3.org/1998/Math/MathML}"
def _element(name: str):
return OxmlElement(f"m:{name}")
def _text_run(text: str):
run = _element("r")
properties = _element("rPr")
style = _element("sty")
style.set(qn("m:val"), "p")
properties.append(style)
run.append(properties)
value = _element("t")
value.text = text
run.append(value)
return run
def _append_children(target, source) -> None:
if source.text and source.text.strip():
target.append(_text_run(source.text.strip()))
for child in source:
_append_mathml(target, child)
if child.tail and child.tail.strip():
target.append(_text_run(child.tail.strip()))
def _script(target, node, kind: str) -> None:
result = _element(kind)
expression = _element("e")
sub = _element("sub")
sup = _element("sup")
children = list(node)
if children:
_append_mathml(expression, children[0])
if len(children) > 1:
_append_mathml(sub, children[1])
if len(children) > 2:
_append_mathml(sup, children[2])
result.append(expression)
if kind in {"sSub", "sSubSup"}:
result.append(sub)
if kind in {"sSup", "sSubSup"}:
result.append(sup if kind == "sSubSup" else sub)
target.append(result)
def _append_mathml(target, node) -> None:
tag = node.tag.removeprefix(MATHML_NS)
children = list(node)
if tag in {"math", "mrow", "mstyle", "semantics", "annotation"}:
_append_children(target, node)
elif tag in {"mi", "mn", "mo", "mtext"}:
target.append(_text_run("".join(node.itertext())))
elif tag == "mfrac":
fraction = _element("f")
numerator = _element("num")
denominator = _element("den")
if children:
_append_mathml(numerator, children[0])
if len(children) > 1:
_append_mathml(denominator, children[1])
fraction.extend((numerator, denominator))
target.append(fraction)
elif tag == "msub":
_script(target, node, "sSub")
elif tag == "msup":
_script(target, node, "sSup")
elif tag in {"msubsup", "munderover"}:
_script(target, node, "sSubSup")
elif tag == "munder":
_script(target, node, "sSub")
elif tag == "mover":
_script(target, node, "sSup")
elif tag == "msqrt":
radical = _element("rad")
properties = _element("radPr")
hide_degree = _element("degHide")
hide_degree.set(qn("m:val"), "1")
properties.append(hide_degree)
degree = _element("deg")
expression = _element("e")
_append_children(expression, node)
radical.extend((properties, degree, expression))
target.append(radical)
elif tag == "mroot":
radical = _element("rad")
degree = _element("deg")
expression = _element("e")
if children:
_append_mathml(expression, children[0])
if len(children) > 1:
_append_mathml(degree, children[1])
radical.extend((degree, expression))
target.append(radical)
elif tag == "mfenced":
delimiter = _element("d")
properties = _element("dPr")
begin = _element("begChr")
begin.set(qn("m:val"), node.attrib.get("open", "("))
end = _element("endChr")
end.set(qn("m:val"), node.attrib.get("close", ")"))
properties.extend((begin, end))
expression = _element("e")
_append_children(expression, node)
delimiter.extend((properties, expression))
target.append(delimiter)
elif tag == "mtable":
matrix = _element("m")
for row_node in children:
row = _element("mr")
for cell_node in list(row_node):
cell = _element("e")
_append_children(cell, cell_node)
row.append(cell)
matrix.append(row)
target.append(matrix)
elif tag in {"mtr", "mtd"}:
_append_children(target, node)
elif tag == "mspace":
target.append(_text_run(" "))
else:
_append_children(target, node)
def latex_to_omml(latex: str):
try:
from latex2mathml.converter import convert
except ImportError as error:
raise RuntimeError(
"Native equations require latex2mathml: py -3 -m pip install latex2mathml"
) from error
mathml = ElementTree.fromstring(convert(latex))
paragraph = _element("oMathPara")
math = _element("oMath")
_append_mathml(math, mathml)
paragraph.append(math)
return paragraph
def clone_omml(element):
return deepcopy(element)
#!/usr/bin/env python3
"""Render patent-style black-and-white flowchart SVGs from draft JSON."""
import argparse
import html
import json
import re
import textwrap
from pathlib import Path
STEP_PATTERN = re.compile(r"\bS\s*(\d+)\b", re.IGNORECASE)
ASCII_ID = re.compile(r"^[A-Za-z][A-Za-z0-9_-]*$")
VAGUE_FINAL_RESULT = re.compile(r"(技术结果|处理结果|最终结果)")
def normalize_step(value: str) -> str:
match = STEP_PATTERN.fullmatch(str(value).strip())
return f"S{match.group(1)}" if match else str(value).strip()
def claim_step_map(claims: list[dict]) -> dict[int, set[str]]:
result = {}
for claim in claims:
number = claim.get("number")
result[number] = {f"S{value}" for value in STEP_PATTERN.findall(claim.get("text", ""))}
return result
def validate_figure(
figure: dict,
steps_by_claim: dict[int, set[str]],
descriptions: list[str],
) -> list[str]:
errors = []
figure_type = figure.get("type")
if figure_type not in {"flowchart", "methodology"}:
errors.append("type must be 'flowchart' or 'methodology'")
if figure.get("orientation", "vertical") not in {"vertical", "horizontal"}:
errors.append("orientation must be 'vertical' or 'horizontal'")
claim_number = figure.get("claim_number", 1)
available_steps = set()
if figure_type == "flowchart":
if claim_number not in steps_by_claim:
errors.append(f"claim_number {claim_number!r} does not exist")
available_steps = steps_by_claim.get(claim_number, set())
nodes = figure.get("nodes", [])
if not nodes:
errors.append("at least one node is required")
ids = [str(node.get("id", "")) for node in nodes]
if len(ids) != len(set(ids)):
errors.append("node ids must be unique")
for node in nodes:
node_id = str(node.get("id", ""))
if not ASCII_ID.fullmatch(node_id):
errors.append(f"invalid node id: {node_id!r}")
if not str(node.get("label", "")).strip():
errors.append(f"node {node_id!r} has an empty label")
step = normalize_step(node.get("claim_step", ""))
if figure_type == "flowchart" and step and step not in available_steps:
errors.append(
f"node {node_id!r} references step {step!r}, not found in claim {claim_number}"
)
node_steps = {
normalize_step(node.get("claim_step", ""))
for node in nodes
if str(node.get("claim_step", "")).strip()
}
if figure_type == "flowchart" and figure.get("complete_claim_flow") and node_steps != available_steps:
missing = sorted(available_steps - node_steps)
extra = sorted(node_steps - available_steps)
if missing:
errors.append(f"complete claim flow is missing steps: {missing}")
if extra:
errors.append(f"complete claim flow contains extra steps: {extra}")
figure_token = f"图{figure.get('number')}"
if not any(figure_token in str(description) for description in descriptions):
errors.append(f"figure description does not reference {figure_token}")
id_set = set(ids)
incoming = {node_id: 0 for node_id in ids}
outgoing = {node_id: 0 for node_id in ids}
for edge in figure.get("edges", []):
source = str(edge.get("from", ""))
target = str(edge.get("to", ""))
if source not in id_set:
errors.append(f"edge source {source!r} does not exist")
if target not in id_set:
errors.append(f"edge target {target!r} does not exist")
if source == target and source:
errors.append(f"self-loop is not allowed for node {source!r}")
if source in outgoing:
outgoing[source] += 1
if target in incoming:
incoming[target] += 1
if len(nodes) > 1 and not figure.get("edges"):
errors.append("multiple nodes require edges")
if len(nodes) > 1:
if not any(value == 0 for value in incoming.values()):
errors.append("flowchart has no start node")
if not any(value == 0 for value in outgoing.values()):
errors.append("flowchart has no end node")
starts = [node_id for node_id, count in incoming.items() if count == 0]
adjacency = {node_id: set() for node_id in ids}
for edge in figure.get("edges", []):
source = str(edge.get("from", ""))
target = str(edge.get("to", ""))
if source in adjacency and target in id_set:
adjacency[source].add(target)
reachable = set(starts)
pending = list(starts)
while pending:
current = pending.pop()
for target in adjacency[current]:
if target not in reachable:
reachable.add(target)
pending.append(target)
disconnected = sorted(id_set - reachable)
if disconnected:
errors.append(f"unreachable nodes from any start node: {disconnected}")
for node in nodes:
node_id = str(node.get("id", ""))
if outgoing.get(node_id) == 0 and VAGUE_FINAL_RESULT.search(str(node.get("label", ""))):
errors.append(
f"end node {node_id!r} uses a vague result name; state the specific detection, "
"estimation, classification, positioning, or control result"
)
return errors
def wrap_label(label: str, width: int = 18) -> list[str]:
chunks = []
for paragraph in str(label).splitlines() or [""]:
chunks.extend(textwrap.wrap(paragraph, width=width) or [""])
return chunks
def layout(figure: dict) -> tuple[dict[str, tuple[int, int, int, int]], int, int]:
orientation = figure.get("orientation", "vertical")
nodes = figure["nodes"]
box_width = 360
gap = 90
margin = 70
positions = {}
max_height = 0
heights = []
for node in nodes:
line_count = len(wrap_label(node["label"]))
height = max(72, 34 + line_count * 24)
heights.append(height)
max_height = max(max_height, height)
if orientation == "vertical":
y = margin + 45
for node, height in zip(nodes, heights):
positions[node["id"]] = (margin, y, box_width, height)
y += height + gap
width = box_width + margin * 2
height = y - gap + margin
else:
x = margin
for node, height in zip(nodes, heights):
positions[node["id"]] = (x, margin + 45, box_width, height)
x += box_width + gap
width = x - gap + margin
height = max_height + margin * 2 + 45
return positions, width, height
def anchor(box: tuple[int, int, int, int], side: str) -> tuple[float, float]:
x, y, width, height = box
points = {
"top": (x + width / 2, y),
"bottom": (x + width / 2, y + height),
"left": (x, y + height / 2),
"right": (x + width, y + height / 2),
}
return points[side]
def render(figure: dict) -> str:
positions, width, height = layout(figure)
orientation = figure.get("orientation", "vertical")
title = f"图{figure['number']} {figure.get('title', '方法流程图')}"
parts = [
'<?xml version="1.0" encoding="UTF-8"?>',
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" '
f'viewBox="0 0 {width} {height}" role="img" aria-label="{html.escape(title)}">',
"<defs>",
'<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" '
'markerHeight="8" orient="auto-start-reverse">',
'<path d="M 0 0 L 10 5 L 0 10 z" fill="#000"/>',
"</marker>",
"</defs>",
'<rect width="100%" height="100%" fill="#fff"/>',
f'<text x="{width / 2}" y="28" text-anchor="middle" '
'font-family="SimSun, Songti SC, serif" font-size="20">'
f"{html.escape(title)}</text>",
]
for edge in figure.get("edges", []):
source_box = positions[edge["from"]]
target_box = positions[edge["to"]]
source_side, target_side = (
("bottom", "top") if orientation == "vertical" else ("right", "left")
)
x1, y1 = anchor(source_box, source_side)
x2, y2 = anchor(target_box, target_side)
parts.append(
f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
'stroke="#000" stroke-width="2" marker-end="url(#arrow)"/>'
)
label = str(edge.get("label", "")).strip()
if label:
parts.append(
f'<text x="{(x1 + x2) / 2 + 8}" y="{(y1 + y2) / 2 - 8}" '
'font-family="SimSun, Songti SC, serif" font-size="16">'
f"{html.escape(label)}</text>"
)
for node in figure["nodes"]:
x, y, box_width, box_height = positions[node["id"]]
parts.append(
f'<rect x="{x}" y="{y}" width="{box_width}" height="{box_height}" '
'rx="0" fill="#fff" stroke="#000" stroke-width="2"/>'
)
lines = wrap_label(node["label"])
start_y = y + box_height / 2 - (len(lines) - 1) * 12
for index, line in enumerate(lines):
parts.append(
f'<text x="{x + box_width / 2}" y="{start_y + index * 24}" '
'text-anchor="middle" dominant-baseline="middle" '
'font-family="SimSun, Songti SC, serif" font-size="18">'
f"{html.escape(line)}</text>"
)
parts.append("</svg>")
return "\n".join(parts)
def load_font(size: int):
from PIL import ImageFont
candidates = (
Path(r"C:\Windows\Fonts\simsun.ttc"),
Path(r"C:\Windows\Fonts\msyh.ttc"),
Path(r"C:\Windows\Fonts\simhei.ttf"),
)
for candidate in candidates:
if candidate.exists():
return ImageFont.truetype(str(candidate), size=size)
return ImageFont.load_default()
def render_png(figure: dict, output: Path) -> None:
from PIL import Image, ImageDraw
positions, width, height = layout(figure)
scale = 2
image = Image.new("RGB", (width * scale, height * scale), "white")
draw = ImageDraw.Draw(image)
title_font = load_font(20 * scale)
body_font = load_font(18 * scale)
edge_font = load_font(16 * scale)
def point(value: float) -> int:
return int(round(value * scale))
title = f"图{figure['number']} {figure.get('title', '方法流程图')}"
title_box = draw.textbbox((0, 0), title, font=title_font)
title_x = (width * scale - (title_box[2] - title_box[0])) / 2
draw.text((title_x, point(8)), title, fill="black", font=title_font)
orientation = figure.get("orientation", "vertical")
for edge in figure.get("edges", []):
source_box = positions[edge["from"]]
target_box = positions[edge["to"]]
source_side, target_side = (
("bottom", "top") if orientation == "vertical" else ("right", "left")
)
x1, y1 = anchor(source_box, source_side)
x2, y2 = anchor(target_box, target_side)
draw.line((point(x1), point(y1), point(x2), point(y2)), fill="black", width=4)
if orientation == "vertical":
arrow = [
(point(x2), point(y2)),
(point(x2 - 7), point(y2 - 12)),
(point(x2 + 7), point(y2 - 12)),
]
else:
arrow = [
(point(x2), point(y2)),
(point(x2 - 12), point(y2 - 7)),
(point(x2 - 12), point(y2 + 7)),
]
draw.polygon(arrow, fill="black")
label = str(edge.get("label", "")).strip()
if label:
draw.text(
(point((x1 + x2) / 2 + 8), point((y1 + y2) / 2 - 20)),
label,
fill="black",
font=edge_font,
)
for node in figure["nodes"]:
x, y, box_width, box_height = positions[node["id"]]
draw.rectangle(
(point(x), point(y), point(x + box_width), point(y + box_height)),
fill="white",
outline="black",
width=4,
)
lines = wrap_label(node["label"])
line_height = 24 * scale
total_height = line_height * len(lines)
text_y = point(y + box_height / 2) - total_height / 2
for line in lines:
text_box = draw.textbbox((0, 0), line, font=body_font)
text_width = text_box[2] - text_box[0]
text_x = point(x + box_width / 2) - text_width / 2
draw.text((text_x, text_y), line, fill="black", font=body_font)
text_y += line_height
output.parent.mkdir(parents=True, exist_ok=True)
image.save(output, format="PNG", dpi=(300, 300))
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("draft", type=Path, help="UTF-8 patent draft JSON")
parser.add_argument("--output-dir", type=Path, required=True, help="SVG output directory")
parser.add_argument("--png", action="store_true", help="Also render PNG files")
args = parser.parse_args()
data = json.loads(args.draft.read_text(encoding="utf-8"))
figures = data.get("figures", [])
if not figures:
parser.error("draft contains no figures")
numbers = [figure.get("number") for figure in figures]
if numbers != list(range(1, len(figures) + 1)):
parser.error(f"figure numbers must be consecutive starting at 1: {numbers}")
steps = claim_step_map(data.get("claims", []))
descriptions = data.get("specification", {}).get("figure_descriptions", [])
all_errors = []
for figure in figures:
errors = validate_figure(figure, steps, descriptions)
all_errors.extend(f"Figure {figure.get('number')}: {error}" for error in errors)
if all_errors:
for error in all_errors:
print(f"ERROR\t{error}")
return 1
args.output_dir.mkdir(parents=True, exist_ok=True)
for figure in figures:
output = args.output_dir / f"figure-{figure['number']}.svg"
output.write_text(render(figure), encoding="utf-8")
print(output)
if args.png:
try:
import PIL
except ImportError as error:
raise SystemExit(
"PNG output requires Pillow: python -m pip install pillow"
) from error
png_output = args.output_dir / f"figure-{figure['number']}.png"
render_png(figure, png_output)
print(png_output)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Render a structured Chinese patent draft JSON file as DOCX."""
import argparse
import json
from pathlib import Path
from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt
from math_to_omml import latex_to_omml
def set_run_font(run, name: str, size: float, bold: bool = False) -> None:
run.font.name = name
run.font.size = Pt(size)
run.bold = bold
run._element.rPr.rFonts.set(qn("w:eastAsia"), name)
def add_heading(document: Document, text: str, level: int = 1) -> None:
paragraph = document.add_paragraph()
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER if level == 1 else WD_ALIGN_PARAGRAPH.LEFT
run = paragraph.add_run(text)
set_run_font(run, "宋体", 16 if level == 1 else 14, bold=True)
def add_body(document: Document, text: str, indent: bool = True) -> None:
paragraph = document.add_paragraph()
paragraph.paragraph_format.line_spacing = 1.5
if indent:
paragraph.paragraph_format.first_line_indent = Cm(0.74)
run = paragraph.add_run(str(text))
set_run_font(run, "宋体", 12)
def add_equation(document: Document, equation: dict) -> None:
paragraph = document.add_paragraph()
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
paragraph.paragraph_format.space_before = Pt(6)
paragraph.paragraph_format.space_after = Pt(6)
latex = equation.get("latex")
if not latex:
raise ValueError(
f"Equation {equation.get('number')} has no latex source for native Office Math"
)
paragraph._p.append(latex_to_omml(latex))
number_run = paragraph.add_run(f" ({equation.get('number', '')})")
set_run_font(number_run, "Cambria Math", 12)
description = equation.get("description")
if description:
add_body(document, description)
def add_page_number(section) -> None:
paragraph = section.footer.paragraphs[0]
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = paragraph.add_run()
begin = OxmlElement("w:fldChar")
begin.set(qn("w:fldCharType"), "begin")
instruction = OxmlElement("w:instrText")
instruction.set(qn("xml:space"), "preserve")
instruction.text = " PAGE "
end = OxmlElement("w:fldChar")
end.set(qn("w:fldCharType"), "end")
run._r.extend((begin, instruction, end))
def configure(document: Document) -> None:
section = document.sections[0]
section.top_margin = Cm(2.54)
section.bottom_margin = Cm(2.54)
section.left_margin = Cm(3.17)
section.right_margin = Cm(3.17)
add_page_number(section)
def add_claims(document: Document, claims: list[dict]) -> None:
add_heading(document, "权利要求书")
for claim in claims:
add_body(document, f"{claim['number']}. {claim['text']}", indent=False)
def add_specification(document: Document, data: dict, figure_dir: Path | None = None) -> None:
add_heading(document, "说明书")
add_heading(document, data.get("title", "[TO CONFIRM: title]"), level=2)
spec = data.get("specification", {})
sections = [
("技术领域", spec.get("technical_field", [])),
("背景技术", spec.get("background", [])),
]
invention = spec.get("invention_content", {})
sections.extend(
[
("发明内容", invention.get("problem", []) + invention.get("solution", [])),
("有益效果", invention.get("beneficial_effects", [])),
("附图说明", spec.get("figure_descriptions", [])),
]
)
for heading, paragraphs in sections:
add_heading(document, heading, level=2)
for paragraph in paragraphs:
add_body(document, paragraph)
equations = spec.get("equations", [])
if equations:
add_heading(document, "公式及符号说明", level=2)
for equation in equations:
add_equation(document, equation)
if figure_dir:
figures = data.get("figures", [])
if figures:
add_heading(document, "说明书附图", level=2)
for figure in figures:
image = figure_dir / f"figure-{figure['number']}.png"
if not image.exists():
continue
paragraph = document.add_paragraph()
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = paragraph.add_run()
run.add_picture(str(image), width=Cm(14))
caption = document.add_paragraph()
caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
caption_run = caption.add_run(f"图{figure['number']} {figure.get('title', '')}")
set_run_font(caption_run, "宋体", 11)
add_heading(document, "具体实施方式", level=2)
for embodiment in spec.get("embodiments", []):
add_heading(document, embodiment.get("heading", "实施例"), level=2)
for paragraph in embodiment.get("paragraphs", []):
add_body(document, paragraph)
def add_figure(
document: Document,
figure: dict,
figure_dir: Path | None,
heading: str | None = None,
) -> bool:
if not figure_dir:
return False
image = figure_dir / f"figure-{figure['number']}.png"
if not image.exists():
return False
if heading:
add_heading(document, heading, level=2)
paragraph = document.add_paragraph()
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = paragraph.add_run()
run.add_picture(str(image), width=Cm(14))
caption = document.add_paragraph()
caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
caption_run = caption.add_run(f"图{figure['number']} {figure.get('title', '')}")
set_run_font(caption_run, "宋体", 11)
return True
def abstract_figure(data: dict) -> dict | None:
number = data.get("abstract_figure_number")
return next(
(figure for figure in data.get("figures", []) if figure.get("number") == number),
None,
)
def add_abstract(
document: Document,
data: dict,
figure_dir: Path | None = None,
include_figure: bool = True,
) -> None:
add_heading(document, "说明书摘要")
add_heading(document, data.get("title", "[TO CONFIRM: title]"), level=2)
add_body(document, data.get("abstract", ""), indent=False)
figure = abstract_figure(data)
if include_figure and figure:
add_figure(document, figure, figure_dir, heading="摘要附图")
def add_abstract_figure(
document: Document,
data: dict,
figure_dir: Path | None,
) -> None:
add_heading(document, "摘要附图")
figure = abstract_figure(data)
if figure:
add_figure(document, figure, figure_dir)
def add_review_appendix(document: Document, data: dict) -> None:
document.add_section(WD_SECTION.NEW_PAGE)
add_heading(document, "起草审查附录")
metadata = data.get("metadata", {})
for key, value in metadata.items():
add_body(document, f"{key}: {value}", indent=False)
add_heading(document, "前提假设", level=2)
for item in data.get("assumptions", []):
add_body(document, f"- {item}", indent=False)
concept = data.get("invention_concept", {})
add_heading(document, "发明构思", level=2)
for key in ("technical_problem", "technical_means", "technical_effect"):
if concept.get(key):
add_body(document, f"{key}: {concept[key]}", indent=False)
ledger = data.get("evidence_ledger", [])
if ledger:
add_heading(document, "证据台账", level=2)
table = document.add_table(rows=1, cols=6)
table.style = "Table Grid"
headers = ("ID", "技术特征", "来源位置", "技术作用", "效果", "支持状态")
for cell, header in zip(table.rows[0].cells, headers):
cell.text = header
keys = ("id", "feature", "source_location", "technical_role", "effect", "support_status")
for item in ledger:
cells = table.add_row().cells
for cell, key in zip(cells, keys):
cell.text = str(item.get(key, ""))
audit = data.get("audit", {})
for heading, key in (
("支持性检查", "support_findings"),
("一致性检查", "consistency_findings"),
("发明人待确认问题", None),
):
add_heading(document, heading, level=2)
items = data.get("inventor_questions", []) if key is None else audit.get(key, [])
for item in items:
add_body(document, f"- {item}", indent=False)
def validate(data: dict) -> None:
required = ("title", "claims", "specification", "abstract")
missing = [key for key in required if key not in data]
if missing:
raise ValueError(f"Missing required keys: {', '.join(missing)}")
numbers = [claim.get("number") for claim in data["claims"]]
if numbers != list(range(1, len(numbers) + 1)):
raise ValueError(f"Claim numbers must be consecutive integers starting at 1: {numbers}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("draft", type=Path, help="UTF-8 JSON draft")
parser.add_argument("--output", type=Path, required=True, help="Output DOCX path")
parser.add_argument(
"--part",
choices=("all", "claims", "specification", "abstract", "abstract-figure"),
default="all",
help="Document part to render",
)
parser.add_argument(
"--figure-dir",
type=Path,
help="Directory containing figure-N.png images for the specification",
)
args = parser.parse_args()
data = json.loads(args.draft.read_text(encoding="utf-8"))
validate(data)
document = Document()
configure(document)
if args.part == "claims":
add_claims(document, data["claims"])
elif args.part == "specification":
add_specification(document, data, args.figure_dir)
elif args.part == "abstract":
add_abstract(document, data, args.figure_dir)
elif args.part == "abstract-figure":
add_abstract_figure(document, data, args.figure_dir)
else:
add_claims(document, data["claims"])
document.add_page_break()
add_specification(document, data, args.figure_dir)
document.add_page_break()
add_abstract(document, data, args.figure_dir)
add_review_appendix(document, data)
args.output.parent.mkdir(parents=True, exist_ok=True)
document.save(args.output)
print(args.output)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Validate traceability, completeness, and quality gates in a patent draft."""
import argparse
import json
import re
from dataclasses import dataclass
from pathlib import Path
SOURCE_ID = re.compile(r"^[PEFC]\d{3,}$")
PLACEHOLDER = re.compile(r"\[(?:TO CONFIRM|待确认)[^\]]*\]", re.IGNORECASE)
VAGUE_RESULT = re.compile(r"(技术结果|处理结果|最终结果)")
QUALITY_THRESHOLDS = {
"evidence_support": 4,
"claim_architecture": 4,
"terminology_consistency": 4,
"enablement_detail": 3,
"technical_effect_reasoning": 3,
}
@dataclass
class Finding:
level: str
code: str
message: str
def add(findings: list[Finding], level: str, code: str, message: str) -> None:
findings.append(Finding(level, code, message))
def validate(data: dict) -> list[Finding]:
findings: list[Finding] = []
required = (
"title",
"metadata",
"source_analysis",
"source_map",
"terminology_ledger",
"formula_inventory",
"figure_inventory",
"evidence_ledger",
"claims",
"claim_feature_map",
"figures",
"specification",
"abstract",
"quality_assessment",
)
for key in required:
if key not in data:
add(findings, "ERROR", "MISSING_KEY", f"缺少顶层字段:{key}。")
claims = data.get("claims", [])
numbers = [claim.get("number") for claim in claims]
if not claims:
add(findings, "ERROR", "NO_CLAIMS", "完整草稿必须包含权利要求。")
elif numbers != list(range(1, len(numbers) + 1)):
add(findings, "ERROR", "CLAIM_SEQUENCE", f"权利要求编号不连续:{numbers}。")
for claim in claims:
text = str(claim.get("text", ""))
if not text.strip():
add(findings, "ERROR", "EMPTY_CLAIM", f"权利要求{claim.get('number')}为空。")
if PLACEHOLDER.search(text):
add(
findings,
"ERROR",
"CLAIM_PLACEHOLDER",
f"权利要求{claim.get('number')}仍含待确认标记。",
)
source_records = data.get("source_map", [])
source_ids = set()
for record in source_records:
source_id = str(record.get("id", ""))
if not SOURCE_ID.fullmatch(source_id):
add(findings, "ERROR", "SOURCE_ID", f"无效来源ID:{source_id!r}。")
if source_id in source_ids:
add(findings, "ERROR", "DUPLICATE_SOURCE_ID", f"来源ID重复:{source_id}。")
source_ids.add(source_id)
if not record.get("locator"):
add(findings, "WARNING", "SOURCE_LOCATOR", f"{source_id}缺少页码、章节或行号。")
canonical_terms = set()
forbidden_aliases = set()
for item in data.get("terminology_ledger", []):
canonical = str(item.get("canonical_zh", "")).strip()
if not canonical:
add(findings, "ERROR", "CANONICAL_TERM", "术语表存在空的canonical_zh。")
elif canonical in canonical_terms:
add(findings, "ERROR", "DUPLICATE_TERM", f"规范术语重复:{canonical}。")
canonical_terms.add(canonical)
forbidden_aliases.update(
str(alias).strip() for alias in item.get("forbidden_aliases", []) if str(alias).strip()
)
ledger_ids = set()
for item in data.get("evidence_ledger", []):
ledger_id = str(item.get("id", ""))
if not ledger_id:
add(findings, "ERROR", "LEDGER_ID", "证据台账条目缺少ID。")
elif ledger_id in ledger_ids:
add(findings, "ERROR", "DUPLICATE_LEDGER_ID", f"证据台账ID重复:{ledger_id}。")
ledger_ids.add(ledger_id)
status = item.get("support_status")
if status not in {"explicit", "inherent", "needs-confirmation", "unsupported"}:
add(findings, "ERROR", "SUPPORT_STATUS", f"{ledger_id}的支持状态无效:{status}。")
referenced = item.get("source_ids", [])
if status in {"explicit", "inherent"} and not referenced:
add(findings, "ERROR", "MISSING_SOURCE_LINK", f"{ledger_id}没有来源ID。")
for source_id in referenced:
if source_ids and source_id not in source_ids:
add(findings, "ERROR", "UNKNOWN_SOURCE_ID", f"{ledger_id}引用未知来源ID:{source_id}。")
mapped_claims = set()
for mapping in data.get("claim_feature_map", []):
claim_number = mapping.get("claim_number")
mapped_claims.add(claim_number)
if claim_number not in numbers:
add(findings, "ERROR", "UNKNOWN_CLAIM", f"特征映射引用不存在的权利要求:{claim_number}。")
if not str(mapping.get("feature", "")).strip():
add(findings, "ERROR", "EMPTY_FEATURE", "权利要求特征映射存在空特征。")
evidence_ids = mapping.get("evidence_ids", [])
if not evidence_ids:
add(
findings,
"ERROR",
"UNMAPPED_FEATURE",
f"权利要求{claim_number}的特征“{mapping.get('feature', '')}”没有证据ID。",
)
for evidence_id in evidence_ids:
if evidence_id not in ledger_ids:
add(
findings,
"ERROR",
"UNKNOWN_EVIDENCE_ID",
f"权利要求{claim_number}引用未知证据ID:{evidence_id}。",
)
for number in numbers:
if number not in mapped_claims:
add(findings, "ERROR", "CLAIM_NOT_MAPPED", f"权利要求{number}没有特征证据映射。")
formal_text = "\n".join(str(claim.get("text", "")) for claim in claims)
formal_text += "\n" + json.dumps(data.get("specification", {}), ensure_ascii=False)
for alias in sorted(forbidden_aliases):
if alias in formal_text:
add(findings, "ERROR", "FORBIDDEN_ALIAS", f"正式文本使用了禁用别名:{alias}。")
source_analysis = data.get("source_analysis", {})
spec = data.get("specification", {})
equations = spec.get("equations", [])
formula_inventory = data.get("formula_inventory", [])
for item in formula_inventory:
source_id = item.get("source_id")
if source_ids and source_id not in source_ids:
add(findings, "ERROR", "FORMULA_INVENTORY_SOURCE", f"公式清单引用未知来源ID:{source_id}。")
if not item.get("disposition"):
add(findings, "ERROR", "FORMULA_DISPOSITION", f"来源公式{source_id}缺少处理去向。")
expected_formula_count = source_analysis.get("formula_count_in_source")
if isinstance(expected_formula_count, int) and expected_formula_count != len(formula_inventory):
add(
findings,
"WARNING",
"FORMULA_INVENTORY_COUNT",
f"来源标记{expected_formula_count}个公式,公式清单记录{len(formula_inventory)}个。",
)
if "equations" not in spec:
add(findings, "ERROR", "EQUATIONS_ARRAY", "说明书必须包含equations数组。")
if source_analysis.get("contains_core_formulas") and not equations:
add(findings, "ERROR", "MISSING_CORE_EQUATIONS", "来源包含核心公式,但说明书未收录公式。")
equation_numbers = [equation.get("number") for equation in equations]
if equation_numbers and equation_numbers != list(range(1, len(equation_numbers) + 1)):
add(findings, "ERROR", "EQUATION_SEQUENCE", f"公式编号不连续:{equation_numbers}。")
for equation in equations:
number = equation.get("number")
if not equation.get("latex"):
add(findings, "ERROR", "EQUATION_LATEX", f"公式{number}缺少可转换的LaTeX源。")
if not equation.get("source_ids"):
add(findings, "ERROR", "EQUATION_SOURCE", f"公式{number}缺少来源ID。")
for source_id in equation.get("source_ids", []):
if source_ids and source_id not in source_ids:
add(findings, "ERROR", "EQUATION_SOURCE", f"公式{number}引用未知来源ID:{source_id}。")
if not equation.get("symbols"):
add(findings, "ERROR", "EQUATION_SYMBOLS", f"公式{number}缺少结构化符号定义。")
if not equation.get("technical_role"):
add(findings, "ERROR", "EQUATION_ROLE", f"公式{number}缺少技术作用说明。")
figures = data.get("figures", [])
for item in data.get("figure_inventory", []):
source_id = item.get("source_id")
if source_ids and source_id not in source_ids:
add(findings, "ERROR", "FIGURE_INVENTORY_SOURCE", f"附图清单引用未知来源ID:{source_id}。")
if not item.get("disposition"):
add(findings, "ERROR", "FIGURE_DISPOSITION", f"来源附图{source_id}缺少处理去向。")
figure_numbers = [figure.get("number") for figure in figures]
if not figures:
add(findings, "ERROR", "NO_FIGURES", "完整草稿必须包含至少一幅专利附图。")
elif figure_numbers != list(range(1, len(figure_numbers) + 1)):
add(findings, "ERROR", "FIGURE_SEQUENCE", f"附图编号不连续:{figure_numbers}。")
abstract_figure = data.get("abstract_figure_number")
if abstract_figure not in figure_numbers:
add(findings, "ERROR", "ABSTRACT_FIGURE", "摘要附图编号未指向现有附图。")
for figure in figures:
if not figure.get("source_ids"):
add(findings, "WARNING", "FIGURE_SOURCE", f"图{figure.get('number')}缺少来源ID或重绘依据。")
for source_id in figure.get("source_ids", []):
if source_ids and source_id not in source_ids:
add(
findings,
"ERROR",
"FIGURE_SOURCE",
f"图{figure.get('number')}引用未知来源ID:{source_id}。",
)
end_nodes = set(str(node.get("id")) for node in figure.get("nodes", []))
for edge in figure.get("edges", []):
end_nodes.discard(str(edge.get("from")))
for node in figure.get("nodes", []):
if str(node.get("id")) in end_nodes and VAGUE_RESULT.search(str(node.get("label", ""))):
add(
findings,
"ERROR",
"VAGUE_FINAL_RESULT",
f"图{figure.get('number')}末端节点使用了模糊结果名称。",
)
for field in ("technical_field", "background", "embodiments", "figure_descriptions"):
if not spec.get(field):
add(findings, "ERROR", "SPEC_SECTION", f"说明书缺少或清空了字段:{field}。")
invention = spec.get("invention_content", {})
for field in ("problem", "solution", "beneficial_effects"):
if not invention.get(field):
add(findings, "ERROR", "INVENTION_CONTENT", f"发明内容缺少:{field}。")
abstract = re.sub(r"\s+", "", str(data.get("abstract", "")))
if not abstract:
add(findings, "ERROR", "EMPTY_ABSTRACT", "说明书摘要为空。")
elif len(abstract) > 300:
add(findings, "WARNING", "ABSTRACT_LENGTH", f"摘要约{len(abstract)}字,建议人工核对篇幅。")
quality = data.get("quality_assessment", {})
if quality.get("status") not in {"review-draft", "incomplete-draft"}:
add(
findings,
"WARNING",
"DRAFT_STATUS",
"quality_assessment.status建议使用review-draft或incomplete-draft。",
)
scores = quality.get("scores", {})
for dimension, threshold in QUALITY_THRESHOLDS.items():
item = scores.get(dimension)
if not isinstance(item, dict) or not isinstance(item.get("score"), int):
add(findings, "ERROR", "QUALITY_SCORE", f"缺少质量评分:{dimension}。")
continue
score = item["score"]
if score < 1 or score > 5:
add(findings, "ERROR", "QUALITY_RANGE", f"{dimension}评分超出1-5:{score}。")
elif score < threshold:
add(
findings,
"ERROR",
"QUALITY_THRESHOLD",
f"{dimension}评分{score},低于交付阈值{threshold}。",
)
if not str(item.get("evidence", "")).strip():
add(findings, "WARNING", "QUALITY_EVIDENCE", f"{dimension}评分缺少依据。")
if source_analysis.get("contains_core_formulas"):
formula_item = scores.get("formula_coverage", {})
if formula_item.get("score", 0) < 4:
add(findings, "ERROR", "FORMULA_SCORE", "存在核心公式时,formula_coverage必须至少为4。")
if figures:
figure_item = scores.get("figure_alignment", {})
if figure_item.get("score", 0) < 4:
add(findings, "ERROR", "FIGURE_SCORE", "存在附图时,figure_alignment必须至少为4。")
return findings
def format_report(findings: list[Finding]) -> str:
if not findings:
return "PASS: 草稿通过结构、溯源和质量门槛检查。\n"
lines = [f"{item.level}\t{item.code}\t{item.message}" for item in findings]
errors = sum(item.level == "ERROR" for item in findings)
warnings = sum(item.level == "WARNING" for item in findings)
lines.extend(("", f"汇总: {errors} 个错误, {warnings} 个警告"))
return "\n".join(lines) + "\n"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("draft", type=Path, help="UTF-8 structured patent draft JSON")
parser.add_argument("--report", type=Path, help="Write the validation report to a file")
parser.add_argument("--json", action="store_true", help="Print findings as JSON")
args = parser.parse_args()
data = json.loads(args.draft.read_text(encoding="utf-8"))
findings = validate(data)
report = format_report(findings)
if args.report:
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(report, encoding="utf-8")
if args.json:
print(json.dumps([item.__dict__ for item in findings], ensure_ascii=False, indent=2))
else:
print(report, end="")
return 1 if any(item.level == "ERROR" for item in findings) else 0
if __name__ == "__main__":
raise SystemExit(main())
Output Contract
Full package
A full-draft job must produce:
权利要求书.docx;说明书.docx;说明书摘要.docx;摘要附图.docx;完整审阅稿.docx;结构化草稿.json;权利要求检查.txt;草稿验证报告.txt;- SVG and PNG files for every generated patent figure.
The structured draft is the source of truth. DOCX files are rendered outputs.
Required traceability
- Every material claim feature maps to at least one source ID.
- Every source-supported core equation has a recorded disposition.
- Every formal term uses the terminology ledger's canonical Chinese form.
- Every numbered claim step maps to one main-flowchart node and one embodiment
explanation.
- Every methodology figure is source-supported or explicitly identified as a
redrawing of supported operations.
Formal-document rules
- Use Chinese for claims, specification, abstract, and figure labels.
- Do not place source IDs, support labels, drafting notes, or quality scores in
formal claims.
- Render formal equations as editable Office Math.
- Use a concrete final method output; do not use “技术结果”“处理结果” or
“最终结果”.
- Keep the abstract concise and free of promotional or unsupported promises.
Quality thresholds
Score each dimension from 1 to 5 and record one sentence of evidence:
- evidence support: at least 4;
- claim architecture: at least 4;
- terminology and dependency consistency: at least 4;
- enablement detail: at least 3;
- technical-effect reasoning: at least 3;
- formula coverage: at least 4 when core formulas exist;
- figure alignment: at least 4 when figures are required.
Any validation ERROR, an unmapped material claim feature, or a missing core formula forces the status incomplete draft.
Delivery note
State that the package requires inventor confirmation and qualified Chinese patent-professional review. Do not describe it as filing-ready merely because the automated checks pass.
Core Principles
1. Treat the paper as technical evidence, not as claim language. 2. Build a full source map before selecting the invention concept. 3. Keep claims no broader than the disclosure can support. 4. Separate essential features from examples, parameters, datasets, and preferred implementations. 5. Tie each technical effect to identified cooperating technical means. 6. Keep terminology, symbols, claim steps, figures, and embodiments aligned. 7. Preserve uncertainty. Mark gaps rather than filling them with plausible inventions. 8. Distinguish drafting quality from patentability. Do not claim novelty, inventiveness, freedom to operate, or filing readiness without professional review. 9. Keep formal application text clean. Put source IDs, support states, scores, and inventor questions in the structured draft and review appendix. 10. Prefer reproducible artifacts: structured JSON, deterministic figures, editable equations, validation reports, and separate DOCX deliverables.
Patent Generation Workflow
Run all applicable stages. A later stage must not silently repair missing evidence from an earlier stage.
Stage 0 - Intake and risk flags
Record the source files, requested deliverables, target jurisdiction, publication status, known disclosure dates, and missing inventor facts.
Output: work/00-intake.json.
Gate: every input file is identified; unknown publication, inventorship, and ownership facts are explicitly marked.
Stage 1 - Source map
Extract the entire substantive disclosure, not only the abstract. Assign stable IDs to text blocks, equations, figures, and supplementary/code evidence. Keep page, section, caption, file, or line locators.
Output: work/01-source-map.json.
Gate: the method, implementation, experiments, limitations, formulas, and methodology figures have all been inspected or marked unavailable.
Stage 2 - Technical inventories
Create:
- a terminology ledger with one canonical Chinese term per object;
- an input-operation-output map;
- a formula inventory with symbols and technical role;
- a figure inventory distinguishing methodology figures from result charts;
- an implementation-gap list.
Output: work/02-technical-inventory.json.
Gate: every core operation has an identified input and output; every core formula and source methodology figure has a disposition.
Stage 3 - Evidence ledger
Convert candidate features into an evidence ledger. Assign support state, source IDs, technical role, effect, and proposed destination.
Output: work/03-evidence-ledger.json.
Gate: no candidate essential feature is unsupported; every needs-confirmation feature becomes an inventor question or is removed.
Stage 4 - Invention concept and claim strategy
Write the one-sentence concept:
technical problem -> cooperating technical means -> specific technical output/effect
Select the principal protected object, essential feature chain, fallback positions, and technically appropriate claim categories. Avoid automatically adding device, medium, system, or use claims.
Output: work/04-claim-strategy.json.
Gate: each essential feature has evidence; the principal claim forms a closed input-operation-output chain.
Stage 5 - Claims
Draft the principal independent claim first. Draft dependent claims in the same technical order, then add other supported categories. Create a claim-feature map from every material limitation to evidence IDs.
Output:
work/05-claims.txt;work/05-claim-map.json.
Gate: scripts/audit_claims.py has no ERROR; every formal claim has mapped source support; no [TO CONFIRM] marker remains in formal claims.
Stage 6 - Specification and figures
Draft the specification around the claims while adding enough implementation detail from the source. Include core formulas, symbol definitions, alternatives, figure descriptions, and embodiments. Generate the main claim-aligned flowchart and supported methodology figures.
Output: work/06-draft.json.
Gate: every claimed term appears in the specification; every claim step is explained; every figure is referenced; every core formula has editable-math source and symbol definitions.
Stage 7 - Abstract and package
Draft the abstract last. Keep terminology aligned with the principal claim. Use the same main figure as the abstract figure and specification figure.
Output: separate Chinese DOCX files, SVG/PNG figures, structured JSON, and audit reports under outputs/.
Gate: scripts/validate_patent_draft.py and scripts/build_patent_package.py complete without errors.
Stage 8 - Final review
Score evidence support, claim architecture, consistency, enablement, technical-effect reasoning, formula coverage, and figure alignment. List unresolved inventor questions and publication risks.
Gate: meet the thresholds in static/core/output-contract.md; otherwise label the package incomplete draft.
Algorithm and Software Inventions
Anchor claims in technical data acquisition, transformations, model or algorithm operations, and a specific technical-domain output or control action. Preserve essential formulas and data dependencies. Keep dataset names, benchmark values, model brands, and exact layer counts in embodiments or narrower claims unless essential.
Apparatus and System Inventions
Identify physical components, connections, spatial relationships, signal or material paths, operating states, and cooperation. Use reference numerals only after the structure is stable. Do not replace structural disclosure with functional module names.
Mixed Inventions
Select one principal protected object and use other categories as supported fallbacks. Keep the same technical relationships across method, apparatus, system, product, and medium claims. Do not duplicate claim categories mechanically.
Process and Material Inventions
Identify raw materials, sequence, conditions, ranges, intermediate states, product characteristics, and measurement methods. Preserve disclosed range boundaries and units. Do not generalize a single example into an unsupported genus or range.
Mixed Project
Inventory every paper, supplement, code directory, inventor note, source figure, and existing patent. Record provenance separately. Source code may support implementation detail but does not automatically establish inventorship or publication status. When paper and patent filenames share a number, verify technical correspondence from their contents.
Pasted Text
Treat the input as incomplete unless the user confirms it contains the full technical disclosure. Create paragraph-level source IDs. Ask for figures, formulas, implementation details, and experimental context only when their absence blocks the requested deliverable; otherwise continue in draft mode.
Selectable PDF
Run scripts/extract_pdf_text.py and preserve page markers. Check extraction coverage across the first, middle, and final pages. Inspect equations and figures against the original PDF because text extraction may lose notation, subscripts, superscripts, or reading order.
Scanned PDF
Do not treat empty extraction as an empty disclosure. Perform OCR with page coordinates when available. Mark low-confidence symbols, equations, tables, and captions. Require manual confirmation for any uncertain content that would materially limit a claim.
Claim Set
Run Stages 0-5 and the claim-related portions of Stage 8. Return the invention concept, evidence ledger, claims, claim-feature map, audit report, and inventor questions. Do not imply that omitted specification support has been checked.
Disclosure Analysis
Run Stages 0-4. Identify protectable technical chains, source-supported fallback positions, implementation gaps, formulas, figures, and inventor questions. Do not generate polished formal claims unless requested.
Full Draft
Run Stages 0-8. Produce the complete output package. Draft claims before the specification and abstract. Keep internal evidence and audit material in the structured draft and combined review appendix, not in formal application text.
Paper-Patent Audit
Build independent source maps for the paper and patent. Compare features using same, narrower-in-patent, broader-in-patent, renamed, patent-only, or paper-only. Cite both locations. Do not infer that patent-only matter is supported by the paper.
import importlib.util
import json
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"validate_patent_draft", ROOT / "scripts" / "validate_patent_draft.py"
)
VALIDATOR = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = VALIDATOR
SPEC.loader.exec_module(VALIDATOR)
def valid_draft():
return {
"title": "一种工业图像缺陷检测方法",
"metadata": {"draft_status": "供复核"},
"source_analysis": {
"contains_core_formulas": True,
"contains_methodology_figures": False,
},
"source_map": [
{"id": "P001", "type": "paper-text", "locator": "第3页", "summary": "流程"},
{"id": "E001", "type": "equation", "locator": "第4页公式1", "summary": "融合"},
],
"terminology_ledger": [
{
"concept": "融合特征",
"canonical_zh": "融合特征",
"source_terms": ["fused feature"],
"forbidden_aliases": [],
}
],
"formula_inventory": [
{
"source_id": "E001",
"source_number": "(1)",
"technical_role": "融合不同尺度特征",
"disposition": "specification-equation-1",
}
],
"figure_inventory": [
{
"source_id": "P001",
"source_number": "方法章节",
"type": "flowchart",
"disposition": "redraw-as-figure-1",
}
],
"evidence_ledger": [
{
"id": "EV1",
"feature": "多尺度特征融合",
"source_ids": ["P001", "E001"],
"support_status": "explicit",
}
],
"claims": [
{
"number": 1,
"text": "一种工业图像缺陷检测方法,其特征在于,包括:S1,获取工业图像;S2,对工业图像进行多尺度特征融合;S3,根据融合特征输出缺陷检测结果。",
}
],
"claim_feature_map": [
{"claim_number": 1, "feature": "多尺度特征融合", "evidence_ids": ["EV1"]}
],
"abstract_figure_number": 1,
"figures": [
{
"number": 1,
"title": "方法流程图",
"type": "flowchart",
"orientation": "vertical",
"claim_number": 1,
"complete_claim_flow": True,
"source_ids": ["P001"],
"nodes": [
{"id": "S1", "label": "S1:获取工业图像", "claim_step": "S1"},
{
"id": "S2",
"label": "S2:进行多尺度特征融合",
"claim_step": "S2",
},
{
"id": "S3",
"label": "S3:输出缺陷检测结果",
"claim_step": "S3",
},
],
"edges": [{"from": "S1", "to": "S2"}, {"from": "S2", "to": "S3"}],
}
],
"specification": {
"technical_field": ["本发明涉及工业视觉检测领域。"],
"background": ["现有方法对小缺陷表征不足。"],
"invention_content": {
"problem": ["提高小缺陷检测能力。"],
"solution": ["采用多尺度特征融合。"],
"beneficial_effects": ["保留不同尺度的缺陷信息。"],
},
"figure_descriptions": ["图1为方法流程图。"],
"equations": [
{
"number": 1,
"source_ids": ["E001"],
"latex": "F=F_1+F_2",
"expression": "F=F1+F2",
"symbols": [
{"symbol": "F", "meaning": "融合特征"},
{"symbol": "F_1", "meaning": "第一尺度特征"},
],
"technical_role": "融合不同尺度特征",
"description": "其中,F表示融合特征,F1和F2表示不同尺度特征。",
}
],
"embodiments": [{"heading": "实施例1", "paragraphs": ["执行上述步骤。"]}],
},
"abstract": "本发明公开一种工业图像缺陷检测方法,通过多尺度特征融合输出缺陷检测结果。",
"quality_assessment": {
"status": "review-draft",
"scores": {
"evidence_support": {"score": 4, "evidence": "特征均有来源。"},
"claim_architecture": {"score": 4, "evidence": "技术链闭合。"},
"terminology_consistency": {"score": 4, "evidence": "术语一致。"},
"enablement_detail": {"score": 3, "evidence": "已说明主要步骤。"},
"technical_effect_reasoning": {"score": 3, "evidence": "效果关联手段。"},
"formula_coverage": {"score": 4, "evidence": "核心公式已收录。"},
"figure_alignment": {"score": 4, "evidence": "附图与步骤一致。"},
}
},
}
class DraftValidationTests(unittest.TestCase):
def test_valid_draft_passes(self):
self.assertEqual([], VALIDATOR.validate(valid_draft()))
def test_unmapped_claim_fails(self):
draft = valid_draft()
draft["claim_feature_map"] = []
codes = {item.code for item in VALIDATOR.validate(draft)}
self.assertIn("CLAIM_NOT_MAPPED", codes)
def test_missing_core_equation_fails(self):
draft = valid_draft()
draft["specification"]["equations"] = []
codes = {item.code for item in VALIDATOR.validate(draft)}
self.assertIn("MISSING_CORE_EQUATIONS", codes)
if __name__ == "__main__":
unittest.main()