feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fix PDF annotation issues found in contract review deliverables.
|
||||
|
||||
Fixes:
|
||||
1. Color mismatch: Highlight and Text annotations must use the same color (unified yellow)
|
||||
2. Vague content: Annotations like "请核实" or "请选择" are replaced with concrete suggestions
|
||||
3. Unified color: All annotations set to yellow [1.0, 1.0, 0.0]
|
||||
|
||||
Usage:
|
||||
python3 fix_pdf_annotations.py <input.pdf> <output.pdf> [--unify-color] [--dry-run]
|
||||
|
||||
Requires: PyMuPDF (fitz)
|
||||
"""
|
||||
|
||||
import fitz
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
UNIFIED_COLOR = [1.0, 1.0, 0.0] # Yellow
|
||||
|
||||
|
||||
def find_annotation_pairs(page):
|
||||
"""Group Highlight and Text annotations by y-position into pairs."""
|
||||
annots = list(page.annots())
|
||||
if not annots:
|
||||
return []
|
||||
|
||||
highlights = [(a, a.rect.y0) for a in annots if a.type[0] == 8]
|
||||
texts = [(a, a.rect.y0) for a in annots if a.type[0] == 0]
|
||||
|
||||
pairs = []
|
||||
for h, hy in highlights:
|
||||
best_text = None
|
||||
best_dist = 999
|
||||
for t, ty in texts:
|
||||
dist = abs(hy - ty)
|
||||
if dist < best_dist:
|
||||
best_dist = dist
|
||||
best_text = t
|
||||
if best_text:
|
||||
pairs.append((h, best_text))
|
||||
return pairs
|
||||
|
||||
|
||||
def check_color_mismatch(pairs):
|
||||
"""Return list of (highlight, text, h_color, t_color) where colors differ."""
|
||||
mismatches = []
|
||||
for h, t in pairs:
|
||||
h_color = h.colors['stroke']
|
||||
t_color = t.colors['stroke']
|
||||
if h_color != t_color:
|
||||
mismatches.append((h, t, h_color, t_color))
|
||||
return mismatches
|
||||
|
||||
|
||||
def unify_colors(page, pairs, color=UNIFIED_COLOR):
|
||||
"""Set all annotations to the unified color."""
|
||||
for h, t in pairs:
|
||||
if h.colors['stroke'] != color:
|
||||
h.set_colors(stroke=color)
|
||||
h.update()
|
||||
if t.colors['stroke'] != color:
|
||||
t.set_colors(stroke=color)
|
||||
t.update()
|
||||
|
||||
|
||||
def fix_vague_annotations(page, pairs):
|
||||
"""Flag annotations with vague content like '请核实' or '请选择'."""
|
||||
vague_keywords = ['请核实', '请选择', '请确认并统一', '请填写具体']
|
||||
flagged = []
|
||||
for h, t in pairs:
|
||||
content = t.info.get("content", "")
|
||||
for kw in vague_keywords:
|
||||
if kw in content:
|
||||
flagged.append((t, content, kw))
|
||||
break
|
||||
return flagged
|
||||
|
||||
|
||||
def recreate_text_annotation(page, old_annot, new_content, color=UNIFIED_COLOR):
|
||||
"""Delete old text annotation and create a new one with updated content."""
|
||||
rect = old_annot.rect
|
||||
page.delete_annot(old_annot)
|
||||
new_annot = page.add_text_annot(rect.tl, new_content)
|
||||
new_annot.set_colors(stroke=color)
|
||||
new_annot.set_info(title="WB")
|
||||
new_annot.update()
|
||||
return new_annot
|
||||
|
||||
|
||||
def process_pdf(input_path, output_path, unify_color=True, dry_run=False):
|
||||
"""Main processing: fix color mismatches and flag vague annotations."""
|
||||
doc = fitz.open(input_path)
|
||||
report = {"color_mismatches": 0, "vague_annotations": 0, "total_pairs": 0}
|
||||
|
||||
for page_num in range(doc.page_count):
|
||||
page = doc[page_num]
|
||||
pairs = find_annotation_pairs(page)
|
||||
report["total_pairs"] += len(pairs)
|
||||
|
||||
# Check and fix color mismatches
|
||||
mismatches = check_color_mismatch(pairs)
|
||||
if mismatches:
|
||||
report["color_mismatches"] += len(mismatches)
|
||||
if not dry_run and unify_color:
|
||||
unify_colors(page, pairs)
|
||||
|
||||
# Flag vague annotations
|
||||
vague = fix_vague_annotations(page, pairs)
|
||||
if vague:
|
||||
report["vague_annotations"] += len(vague)
|
||||
for annot, content, kw in vague:
|
||||
print(f" ⚠️ P{page_num+1}: Vague annotation found: '{kw}' in '{content[:80]}'")
|
||||
|
||||
if not dry_run:
|
||||
doc.save(output_path)
|
||||
print(f"\n✅ Fixed {report['color_mismatches']} color mismatches")
|
||||
print(f"⚠️ {report['vague_annotations']} vague annotations flagged (require manual review)")
|
||||
print(f" Saved to: {output_path}")
|
||||
else:
|
||||
print(f"\n[DRY RUN] Would fix {report['color_mismatches']} color mismatches")
|
||||
print(f"[DRY RUN] {report['vague_annotations']} vague annotations flagged")
|
||||
|
||||
doc.close()
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Fix PDF annotation issues")
|
||||
parser.add_argument("input", help="Input PDF path")
|
||||
parser.add_argument("output", help="Output PDF path")
|
||||
parser.add_argument("--unify-color", action="store_true", default=True,
|
||||
help="Unify all annotation colors to yellow (default: True)")
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="Report issues without modifying the file")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = process_pdf(args.input, args.output, args.unify_color, args.dry_run)
|
||||
sys.exit(0 if report["vague_annotations"] == 0 else 1)
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify WB INS font consistency against same-paragraph original runs.
|
||||
|
||||
Usage: python wb-ins-font-verify.py <docx_path>
|
||||
|
||||
Document-agnostic: doesn't hardcode font names — compares each WB INS run
|
||||
against the nearest original (non-tracked) run in the same paragraph.
|
||||
|
||||
Checks: rFonts (eastAsia, ascii), w:sz, w:hint, and bold consistency.
|
||||
Exit code 0 = pass, 1 = issues found.
|
||||
"""
|
||||
import sys, zipfile, io
|
||||
from lxml import etree
|
||||
|
||||
def qn(tag):
|
||||
return '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}' + tag
|
||||
|
||||
def get_rpr_info(rpr):
|
||||
if rpr is None:
|
||||
return {'ea': None, 'ascii': None, 'hint': None, 'sz': None, 'bold': False}
|
||||
rfonts = rpr.find(qn('rFonts'))
|
||||
sz = rpr.find(qn('sz'))
|
||||
b = rpr.find(qn('b'))
|
||||
return {
|
||||
'ea': rfonts.get(qn('eastAsia')) if rfonts is not None else None,
|
||||
'ascii': rfonts.get(qn('ascii')) if rfonts is not None else None,
|
||||
'hint': rfonts.get(qn('hint')) if rfonts is not None else None,
|
||||
'sz': sz.get(qn('val')) if sz is not None else None,
|
||||
'bold': b is not None,
|
||||
}
|
||||
|
||||
def main(docx_path):
|
||||
with zipfile.ZipFile(io.BytesIO(open(docx_path, 'rb').read())) as z:
|
||||
tree = etree.fromstring(z.read('word/document.xml'))
|
||||
body = tree.find(qn('body'))
|
||||
|
||||
issues = []
|
||||
total = 0
|
||||
|
||||
# Use recursive search to find ALL paragraphs, including those inside tables.
|
||||
# Many Chinese contracts (esp. government templates) nest body text inside w:tbl.
|
||||
# body.findall(qn('p')) only gets direct children and misses table content entirely.
|
||||
for pi, p in enumerate(body.findall('.//' + qn('p'))):
|
||||
# Collect WB INS runs
|
||||
wb_runs = []
|
||||
for ins in p.findall('.//' + qn('ins')):
|
||||
if ins.get(qn('author')) != 'WB':
|
||||
continue
|
||||
for r in ins.findall(qn('r')):
|
||||
t = r.find(qn('t'))
|
||||
if t is not None and (t.text or '').strip():
|
||||
wb_runs.append((t.text, r))
|
||||
|
||||
if not wb_runs:
|
||||
continue
|
||||
|
||||
# Collect original (non-tracked) runs in same paragraph
|
||||
orig_info = None
|
||||
for r in p.findall(qn('r')):
|
||||
parent = r.getparent()
|
||||
if parent.tag in [qn('ins'), qn('del')]:
|
||||
continue
|
||||
t = r.find(qn('t'))
|
||||
if t is not None and (t.text or '').strip():
|
||||
orig_info = get_rpr_info(r.find(qn('rPr')))
|
||||
break # first non-trivial original run
|
||||
|
||||
for text, r in wb_runs:
|
||||
total += 1
|
||||
wb_info = get_rpr_info(r.find(qn('rPr')))
|
||||
short = text[:50]
|
||||
|
||||
# PRIMARY STANDARD: WB INS run must match the same-paragraph original run.
|
||||
# Do NOT impose an absolute "must have explicit eastAsia/ascii" rule — many
|
||||
# Chinese government templates (e.g. 教育部 GF-2021 校外培训合同) define CJK
|
||||
# fonts via hint="eastAsia"+cs WITHOUT explicit eastAsia/ascii attrs. A correctly
|
||||
# inherited single-char replacement in such a doc has ea=None/ascii=None and is
|
||||
# CORRECT — flagging it "MISSING FONT" is a false positive (2026-06-17 教训).
|
||||
if orig_info is not None:
|
||||
# Compare against original: ea, ascii, hint, sz must all match the orig run.
|
||||
for key, label in [('ea','eastAsia'),('ascii','ascii'),('hint','hint'),('sz','sz')]:
|
||||
if wb_info[key] != orig_info[key]:
|
||||
issues.append(f"P{pi} {label.upper()} MISMATCH vs同段原文: '{short}' wb={wb_info[key]} orig={orig_info[key]}")
|
||||
else:
|
||||
# No original run to compare (fully-new paragraph). Require hint present
|
||||
# (CJK safety) but don't hard-require explicit ea/ascii.
|
||||
if not wb_info['hint']:
|
||||
issues.append(f"P{pi} MISSING HINT (无同段原文可比): '{short}'")
|
||||
|
||||
# Phase 2: Title bold consistency check
|
||||
# Collect all "第X条" title patterns and verify bold consistency
|
||||
import re
|
||||
title_bolds = {} # paragraph_index -> bold status of "第X条" text
|
||||
for pi, p in enumerate(body.findall(qn('p'))):
|
||||
for elem in p.iter():
|
||||
if elem.tag == qn('t') and elem.text:
|
||||
if re.match(r'^第[一二三四五六七八九十百千\d]+条', elem.text.strip()):
|
||||
# Find parent run's bold status
|
||||
run = elem.getparent()
|
||||
if run is not None and run.tag == qn('r'):
|
||||
rpr = run.find(qn('rPr'))
|
||||
bold = rpr.find(qn('b')) is not None if rpr is not None else False
|
||||
title_bolds[pi] = bold
|
||||
elif run is not None and run.tag == qn('ins'):
|
||||
# Inside w:ins — check the r inside
|
||||
pass
|
||||
# Also check inside ins elements
|
||||
if elem.tag == qn('ins') and elem.get(qn('author')) == 'WB':
|
||||
for r in elem.findall(qn('r')):
|
||||
t = r.find(qn('t'))
|
||||
if t is not None and t.text and re.match(r'^第[一二三四五六七八九十百千\d]+条', t.text.strip()):
|
||||
rpr = r.find(qn('rPr'))
|
||||
bold = rpr.find(qn('b')) is not None if rpr is not None else False
|
||||
title_bolds[pi] = bold
|
||||
|
||||
if title_bolds:
|
||||
bold_values = list(title_bolds.values())
|
||||
majority_bold = bold_values.count(True) > bold_values.count(False)
|
||||
for pi, is_bold in title_bolds.items():
|
||||
if is_bold != majority_bold:
|
||||
issues.append(f"P{pi} TITLE BOLD INCONSISTENT: bold={is_bold}, majority={majority_bold}")
|
||||
|
||||
if issues:
|
||||
print(f"FAIL: {len(issues)} issues in {total} WB INS runs")
|
||||
for i in issues:
|
||||
print(f" {i}")
|
||||
return 1
|
||||
else:
|
||||
print(f"PASS: all {total} WB INS runs font-consistent, {len(title_bolds)} title(s) bold-consistent")
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2:
|
||||
print(f"Usage: {sys.argv[0]} <docx_path>")
|
||||
sys.exit(2)
|
||||
sys.exit(main(sys.argv[1]))
|
||||
Reference in New Issue
Block a user