translate, done with 众生都是既然众生 and 佛教徒的人生态度

This commit is contained in:
iacore
2026-06-15 17:03:59 +08:00
parent 5c87509dcc
commit c9cbf1c2fe
24 changed files with 2873 additions and 191 deletions
+260
View File
@@ -0,0 +1,260 @@
"""
Generate bilingual.dj for 佛教徒的人生态度.
Source: Chinese from DOCX manuscript.
Target: English from PDF typeset.
Strategy: find each DOCX English paragraph in PDF body, extract
the PDF text region for that paragraph using position boundaries.
"""
import re, subprocess, os
DOCX = "/home/user/documents/mpi/translate-files/佛教徒的人生态度/定稿 佛教徒的人生态度 善鑫慧炬照禅道靖妙一观轩慈德20260527.docx"
PDF = "/home/user/documents/mpi/translate-files/佛教徒的人生态度/0607-二排-果澄-佛教徒的人生态度-一校-多人-0607.pdf"
OUT_DIR = "/home/user/documents/mpi/translate-files/佛教徒的人生态度"
def has_cjk(s):
return any('\u4e00' <= c <= '\u9fff' for c in s)
def pandoc(path):
r = subprocess.run(['pandoc', path, '-f', 'docx', '-t', 'plain', '--wrap=none'],
capture_output=True, text=True)
return r.stdout
def extract_docx_pairs(text):
"""Return [(cn_para, en_para), ...] from body onwards."""
lines = text.split('\n')
body_start = None
for i, l in enumerate(lines):
if '生活在这个世间' in l:
body_start = i
break
pairs = []
i = body_start
while i < len(lines):
cn = lines[i].strip()
if not cn or not has_cjk(cn):
i += 1
continue
en = ''
if i + 2 < len(lines) and lines[i+1].strip() == '':
ec = lines[i+2].strip()
if ec and not has_cjk(ec):
en = ec
i += 3
else:
i += 1
else:
i += 1
continue
pairs.append((cn, en))
return pairs
def extract_pdf_body(text):
"""Return cleaned PDF body string."""
lines = text.split('\n')
body_start = None
for i, l in enumerate(lines):
if 'iving in this world' in l.strip():
body_start = i
break
slug_re = re.compile(r'佛教徒的人生态度.*indd \d+')
hdr_re = re.compile(r'^(The Life Attitudes of Buddhists|The Mindful Peace Academy Collection)$')
pn_re = re.compile(r'^\d{1,3}$')
tl = []
for i in range(body_start, len(lines)):
s = lines[i].strip()
if not s or s == '\x0c':
continue
if slug_re.search(s) or hdr_re.match(s) or pn_re.match(s):
continue
tl.append(s)
# Join hyphenation breaks — handle consecutive breaks
joined = []
i = 0
while i < len(tl):
line = tl[i].rstrip()
if line.endswith('-') and i + 1 < len(tl):
n = tl[i+1].lstrip()
if n and n[0].islower():
merged = line[:-1] + n
# Check if MORE consecutive breaks follow
j = i + 2
while j < len(tl) and merged.rstrip().endswith('-'):
nn = tl[j].lstrip()
if nn and nn[0].islower():
merged = merged.rstrip()[:-1] + nn
j += 1
else:
break
joined.append(merged)
i = j
continue
joined.append(line)
i += 1
body = ' '.join(joined)
body = re.sub(r'\s+', ' ', body).strip()
body = body.replace('L iving', 'Living')
return body
def norm(s):
s = re.sub(r'\s+', ' ', s).strip().lower()
s = s.replace('\u201c', '"').replace('\u201d', '"')
s = s.replace('\u2018', "'").replace('\u2019', "'")
return s
def find_positions(pairs, pdf_body):
"""For each DOCX English para, find start position in PDF body.
Returns list of (start_pos or None, matched_text or None).
"""
positions = []
last_pos = 0
for cn, en in pairs:
needle = norm(en)
haystack = norm(pdf_body[last_pos:])
# Try full match
idx = haystack.find(needle)
if idx < 0:
# Try first 80 chars
key = needle[:80]
idx = haystack.find(key)
if idx < 0:
# Try first 40 chars
key = needle[:40]
idx = haystack.find(key)
if idx < 0:
# Try first 25 chars
key = needle[:25]
idx = haystack.find(key)
if idx >= 0:
pos = last_pos + idx
positions.append(pos)
last_pos = pos + max(len(needle), 30)
else:
positions.append(None)
return positions
def extract_segments(pdf_body, positions):
"""For each position, extract the PDF text region.
Region extends from positions[i] to positions[i+1] (or end),
trimmed to avoid bleeding into the next paragraph.
"""
segments = []
for i, pos in enumerate(positions):
if pos is None:
segments.append(None)
continue
start = pos
end = len(pdf_body)
for j in range(i + 1, len(positions)):
if positions[j] is not None:
end = positions[j]
break
raw = pdf_body[start:end].strip()
# Trim: if raw contains what looks like the NEXT paragraph's heading,
# cut at the last sentence boundary before it.
# Headings match patterns like: "I Passive", "1) Expressions", "1. The Definitions"
heading_pattern = re.compile(
r'\s+(?=[IVX]+\.?\s+[A-Z]' # Roman numeral chapter
r'|\d+\)\s+[A-Z]' # 1) Sub-heading
r'|\(\d+\)\s+[A-Z]' # (1) Sub-heading
r'|\d+\.\s+[A-Z][a-z]+.*?(?:Passive|Pessimism|Abstinence|Focus|Benefit|Transcending|Love|Adapting|Conclusion|Desire|Being|What|How|The|Buddhism|Set|Free|A Middle)' # Numbered heading
r')'
)
m = heading_pattern.search(raw)
if m:
# Cut before this heading
raw = raw[:m.start()].strip()
segments.append(raw)
return segments
def generate(pairs, segments, out_path):
lines = []
# Title
lines.append('# 佛教徒的人生态度')
lines.append('# The Life Attitudes of Buddhists')
lines.append('')
lines.append('------2014年秋讲于第九届菩提静修营')
lines.append('---Lecture Given at the 9th Bodhi Meditation Retreat, 2014')
lines.append('')
lines.append(' 济群法师 ')
lines.append('Master Jiqun')
lines.append('')
# TOC from DOCX
lines.append('- 一、消极还是积极')
lines.append('- 二、悲观还是乐观')
lines.append('- 三、禁欲还是纵欲')
lines.append('- 四、重生还是重死')
lines.append('- 五、自利还是利他')
lines.append('- 六、出世还是入世')
lines.append('- 七、无情还是多情')
lines.append('- 八、随缘还是进取')
lines.append('- 九、结束语')
lines.append('')
lines.append('- I. Passive or Proactive')
lines.append('- II. Pessimism or Optimism')
lines.append('- III. Abstinence or Indulgence')
lines.append('- IV. Focus on Life or on Death')
lines.append('- V. Benefit Oneself or Benefit Others')
lines.append('- VI. Transcending the World or Engaging with the World')
lines.append('- VII. To Love or Not to Love')
lines.append('- VIII. Adapting to Conditions or Striving for Progress')
lines.append('- IX. Conclusion')
lines.append('')
# Body
for (cn, en), seg in zip(pairs, segments):
target = seg if seg else en
lines.append(cn)
lines.append(target)
lines.append('')
with open(out_path, 'w') as f:
f.write('\n'.join(lines))
matched = sum(1 for s in segments if s is not None)
print(f"Written: {out_path}")
print(f" Paragraphs: {len(pairs)}, matched from PDF: {matched}, fallback to DOCX: {len(pairs) - matched}")
if __name__ == '__main__':
print("Extracting DOCX...")
docx_text = pandoc(DOCX)
pairs = extract_docx_pairs(docx_text)
print(f" Pairs: {len(pairs)}")
print("Extracting PDF...")
r = subprocess.run(['pdftotext', '-layout', PDF, '/tmp/_bilingual_pdf.txt'], check=True)
with open('/tmp/_bilingual_pdf.txt') as f:
pdf_raw = f.read()
pdf_body = extract_pdf_body(pdf_raw)
print(f" PDF body: {len(pdf_body)} chars")
print("Finding positions...")
positions = find_positions(pairs, pdf_body)
found = sum(1 for p in positions if p is not None)
print(f" Found: {found}/{len(pairs)}")
print("Extracting segments...")
segments = extract_segments(pdf_body, positions)
out = os.path.join(OUT_DIR, 'bilingual.dj')
generate(pairs, segments, out)
+128
View File
@@ -0,0 +1,128 @@
"""Generate bilingual.dj from DOCX manuscript only (no PDF).
Source: Chinese from DOCX. Target: English from DOCX.
"""
import re, subprocess, os
DOCX = "/home/user/documents/mpi/translate-files/佛教徒的人生态度/定稿 佛教徒的人生态度 善鑫慧炬照禅道靖妙一观轩慈德20260527.docx"
OUT_DIR = "/home/user/documents/mpi/translate-files/佛教徒的人生态度"
def has_cjk(s):
return any('\u4e00' <= c <= '\u9fff' for c in s)
def pandoc(path):
r = subprocess.run(['pandoc', path, '-f', 'docx', '-t', 'plain', '--wrap=none'],
capture_output=True, text=True)
return r.stdout
def extract_pairs(text):
"""Return [(cn_para, en_para), ...] from body onwards."""
lines = text.split('\n')
body_start = None
for i, l in enumerate(lines):
if '生活在这个世间' in l:
body_start = i
break
pairs = []
i = body_start
while i < len(lines):
cn = lines[i].strip()
if not cn or not has_cjk(cn):
i += 1
continue
en = ''
if i + 2 < len(lines) and lines[i+1].strip() == '':
ec = lines[i+2].strip()
if ec and not has_cjk(ec):
en = ec
i += 3
else:
i += 1
else:
i += 1
continue
pairs.append((cn, en))
return pairs
SANSKRIT = [
'bodhisattva', 'bodhicitta', 'samsara', 'Dharma', 'karma',
'nirvana', 'Sangha', 'sutra', 'Mahayana', 'Sravaka',
'Vinaya', 'Lamrim', 'Ksitigarbha', 'Samantabhadra',
'Chan', 'Arhatship', 'Theravada',
]
def apply_fixes(en_text, italicized):
"""Apply typesetting fixes to English text."""
# Fix: "2.How" → "2. How"
en_text = re.sub(r'(\d)\.([A-Z][a-z])', r'\1. \2', en_text)
# Fix: "said,"When → "said, "When
en_text = re.sub(r'(said|says),"', r'\1, "', en_text)
# Italicize Sanskrit on first occurrence
for term in SANSKRIT:
if term not in italicized:
pattern = re.compile(r'\b' + re.escape(term) + r'\b')
m = pattern.search(en_text)
if m:
s, e = m.start(), m.end()
en_text = en_text[:s] + '*' + en_text[s:e] + '*' + en_text[e:]
italicized.add(term)
return en_text
def generate(pairs, out_path):
italicized = set()
lines = []
# Title
lines.append('# 佛教徒的人生态度')
lines.append('# The Life Attitudes of Buddhists')
lines.append('')
lines.append('------2014年秋讲于第九届菩提静修营')
lines.append('---Lecture Given at the 9th Bodhi Meditation Retreat, 2014')
lines.append('')
lines.append(' 济群法师 ')
lines.append('Master Jiqun')
lines.append('')
# TOC
lines.append('- 一、消极还是积极')
lines.append('- 二、悲观还是乐观')
lines.append('- 三、禁欲还是纵欲')
lines.append('- 四、重生还是重死')
lines.append('- 五、自利还是利他')
lines.append('- 六、出世还是入世')
lines.append('- 七、无情还是多情')
lines.append('- 八、随缘还是进取')
lines.append('- 九、结束语')
lines.append('')
lines.append('- I. Passive or Proactive')
lines.append('- II. Pessimism or Optimism')
lines.append('- III. Abstinence or Indulgence')
lines.append('- IV. Focus on Life or on Death')
lines.append('- V. Benefit Oneself or Benefit Others')
lines.append('- VI. Transcending the World or Engaging with the World')
lines.append('- VII. To Love or Not to Love')
lines.append('- VIII. Adapting to Conditions or Striving for Progress')
lines.append('- IX. Conclusion')
lines.append('')
# Body
for cn, en in pairs:
en_fixed = apply_fixes(en, italicized)
lines.append(cn)
lines.append(en_fixed)
lines.append('')
with open(out_path, 'w') as f:
f.write('\n'.join(lines))
print(f"Written: {out_path}")
print(f" Paragraphs: {len(pairs)}")
print(f" Sanskrit italicized: {sorted(italicized)}")
if __name__ == '__main__':
print("Extracting DOCX...")
text = pandoc(DOCX)
pairs = extract_pairs(text)
print(f" Pairs: {len(pairs)}")
out = os.path.join(OUT_DIR, 'bilingual.dj')
generate(pairs, out)
+191
View File
@@ -0,0 +1,191 @@
"""Generate bilingual.dj from DOCX for 「生命也可以被设计的」."""
import re, subprocess, os, hashlib
DOCX = "/home/user/documents/mpi/translate-files/生命也可以被设计的/中英文定稿-260324-生命也是可以被设计的-妙一宽山静雅初翻 慈鎏妙一审议 宽山定稿.docx"
OUT_DIR = "/home/user/documents/mpi/translate-files/生命也可以被设计的"
def has_cjk(s):
return any('\u4e00' <= c <= '\u9fff' for c in s)
def pandoc(path):
r = subprocess.run(['pandoc', path, '-f', 'docx', '-t', 'plain', '--wrap=none'],
capture_output=True, text=True)
return r.stdout
def split_toc_line(line):
"""Split ' 一、教育是为了育人 EDUCATION IS ABOUT NURTURING THE PERSON 3'
into (cn, en). Split at CJK→ASCII uppercase boundary."""
s = line.strip()
# Remove trailing page number
s = re.sub(r'\s+\d+\s*$', '', s)
# Find boundary: last CJK char followed by space(s) + ASCII uppercase
m = re.match(r'^(.+[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef\)])\s+([A-Z].+)$', s)
if m:
return m.group(1).strip(), m.group(2).strip()
return None, None
def join_split_paragraphs(lines):
"""Join consecutive CJK lines that were split by page breaks.
Only join when first line is long (>30 chars) to avoid joining headings."""
result = []
i = 0
while i < len(lines):
line = lines[i]
s = line.strip()
# Page-break splits happen in mid-paragraph; headings are short.
# Only join when first line long AND doesn't end with terminal punctuation.
long_and_abrupt = (s and has_cjk(s) and len(s) > 30
and not re.search(r'[。!?:)\u201d\u2019]$', s))
if long_and_abrupt and i + 2 < len(lines) and lines[i+1].strip() == '' and has_cjk(lines[i+2]):
result.append(line.rstrip() + lines[i+2].lstrip())
i += 3
else:
result.append(line)
i += 1
return result
def extract_toc_entries(text):
"""Return (cn_entries, en_entries) lists from TOC area."""
lines = text.split('\n')
toc_start = None
toc_end = None
for i, l in enumerate(lines):
s = l.strip()
if s.startswith('一、') and ('EDUCATION' in s or 'NURTURING' in s):
if toc_start is None:
toc_start = i
# TOC entries have page numbers at end
if toc_start is not None and s and has_cjk(s) and re.search(r'\d+$', s):
toc_end = i
elif toc_start is not None and toc_end is not None and s and not re.search(r'\d+$', s) and has_cjk(s):
break
cn_entries = []
en_entries = []
for i in range(toc_start, toc_end + 1):
s = lines[i]
cn, en = split_toc_line(s)
if cn and en:
cn_entries.append(cn)
en_entries.append(en)
return cn_entries, en_entries
def extract_body_pairs(text):
"""Return [(cn, en), ...] pairs from body paragraphs and headings."""
lines = text.split('\n')
# Find body start: first Chinese paragraph after TOC
body_start = None
for i, l in enumerate(lines):
if '现在是一个浮躁的时代' in l:
body_start = i
break
# Join split paragraphs first
pre = lines[:body_start]
body = lines[body_start:]
body = join_split_paragraphs(body)
# Extract pairs: pattern is cn, blank, en, blank
pairs = []
i = 0
while i < len(body):
s = body[i].strip()
if not s:
i += 1
continue
if not has_cjk(s):
i += 1
continue
# Chinese line found, look for English after blank
en = ''
if i + 2 < len(body) and body[i+1].strip() == '':
ec = body[i+2].strip()
if ec and not has_cjk(ec):
en = ec
i += 3
else:
i += 1
else:
i += 1
continue
pairs.append((s, en))
return pairs
SANSKRIT = [
'bodhisattva', 'bodhicitta', 'samsara', 'Dharma', 'karma',
'nirvana', 'Sangha', 'sutra', 'Mahayana', 'Sravaka',
'Vinaya', 'Lamrim', 'Ksitigarbha', 'Samantabhadra',
'Chan', 'Arhatship', 'Theravada', 'buddha', 'Buddha',
'buddhas', 'Buddhas', 'Bodhisattva', 'Bodhisattvas',
]
def apply_fixes(en_text, italicized):
"""Apply typesetting fixes to English text."""
# Fix: "2.How" → "2. How"
en_text = re.sub(r'(\d)\.([A-Z][a-z])', r'\1. \2', en_text)
# Fix: "said,\"When → "said, \"When
en_text = re.sub(r'(said|says),\"', r'\1, "', en_text)
# Fix: "...understanding.Yet..." → "...understanding. Yet..."
en_text = re.sub(r'\.([A-Z][a-z])', r'. \1', en_text)
# Italicize Sanskrit on first occurrence
for term in SANSKRIT:
if term not in italicized:
pattern = re.compile(r'\b' + re.escape(term) + r'\b')
m = pattern.search(en_text)
if m:
s, e = m.start(), m.end()
en_text = en_text[:s] + '*' + en_text[s:e] + '*' + en_text[e:]
italicized.add(term)
return en_text
def generate(toc_cn, toc_en, pairs, out_path):
italicized = set()
lines = []
# Title
lines.append('# 生命也是可以被设计的')
lines.append('# Life Can Also Be Designed')
lines.append('')
lines.append('济群法师 2025年冬为母爱书院开示')
lines.append('A teaching given by the Master Jiqun in the winter of 2025 at Amrita Retreat Center for Motherly Love Academy')
lines.append('')
# TOC
for e in toc_cn:
lines.append(f'- {e}')
lines.append('')
for e in toc_en:
lines.append(f'- {e}')
lines.append('')
# Body
for cn, en in pairs:
en_fixed = apply_fixes(en, italicized)
lines.append(cn)
lines.append(en_fixed)
lines.append('')
with open(out_path, 'w') as f:
f.write('\n'.join(lines))
print(f"Written: {out_path}")
print(f" TOC entries: {len(toc_cn)}")
print(f" Body pairs: {len(pairs)}")
print(f" Sanskrit italicized: {sorted(italicized)}")
if __name__ == '__main__':
print("Extracting DOCX...")
text = pandoc(DOCX)
print("Extracting TOC...")
toc_cn, toc_en = extract_toc_entries(text)
for cn, en in zip(toc_cn, toc_en):
print(f" {cn}{en}")
print("Extracting body...")
pairs = extract_body_pairs(text)
print(f" Pairs: {len(pairs)}")
out = os.path.join(OUT_DIR, 'bilingual.dj')
generate(toc_cn, toc_en, pairs, out)
-57
View File
@@ -1,57 +0,0 @@
#!/usr/bin/env fish
# Generate bilingual.dj from source.dj + target.dj in a directory
# Usage: gen-bilingual <dir-containing-source.dj-and-target.dj>
set dir (realpath $argv[1])
set src "$dir/source.dj"
set tgt "$dir/target.dj"
set out "$dir/bilingual.dj"
if not test -f $src; or not test -f $tgt
echo "Missing source.dj or target.dj in $dir" >&2
exit 1
end
python3 -c "
import sys
src_path, tgt_path, out_path = sys.argv[1:]
with open(src_path) as f:
src_lines = f.read().splitlines()
with open(tgt_path) as f:
tgt_lines = f.read().splitlines()
src_toc = src_lines[4:32]
tgt_toc = tgt_lines[4:32]
src_body = src_lines[33:]
tgt_body = tgt_lines[33:]
out = []
out.append(src_lines[0])
out.append(tgt_lines[0])
out.append('')
out.append(src_lines[2])
out.append(tgt_lines[2])
out.append('')
for line in src_toc:
out.append(line)
out.append('')
for line in tgt_toc:
out.append(line)
out.append('')
for s, t in zip(src_body, tgt_body):
if s == '' and t == '':
continue
out.append(s)
out.append(t)
out.append('')
with open(out_path, 'w') as f:
f.write(chr(10).join(out))
" $src $tgt $out
echo $out
+169
View File
@@ -0,0 +1,169 @@
"""Compare manuscript (DOCX English body) against typeset (PDF English body).
Usage: python3 scripts/proofread-pdf.py <docx_path> <pdf_path>
Output: sentences from DOCX not found in PDF, and word-level changes within matched sentences.
"""
import re, sys, subprocess
def extract_docx_en(path):
with open(path) as f:
lines = f.readlines()
body_start = None
for i, line in enumerate(lines):
if '生活在这个世间' in line:
body_start = i
break
if body_start is None:
sys.exit("Could not find body start in DOCX")
docx_en = []
skip_next = 0
for i in range(body_start, len(lines)):
if skip_next > 0:
skip_next -= 1
continue
line = lines[i].strip()
if not line:
continue
has_cjk = any('\u4e00' <= c <= '\u9fff' for c in line)
if has_cjk:
if i + 1 < len(lines) and lines[i+1].strip() == '':
if i + 2 < len(lines):
en_line = lines[i+2].strip()
if en_line and not any('\u4e00' <= c <= '\u9fff' for c in en_line):
docx_en.append(en_line)
skip_next = 2
else:
docx_en.append(line)
# Split into sentences, filter out headings
text = ' '.join(docx_en)
sentences = re.split(r'(?<=[.!?"”])\s+', text)
return [(s.strip(), len(s.strip())) for s in sentences if len(s.strip()) >= 20]
def extract_pdf_en(path):
with open(path) as f:
lines = f.readlines()
body_start = None
for i, line in enumerate(lines):
if 'iving in this world' in line:
body_start = i
break
if body_start is None:
sys.exit("Could not find body start in PDF")
slug_re = re.compile(r'佛教徒的人生态度.*indd \d+')
header_re = re.compile(r'^(The Life Attitudes of Buddhists|The Mindful Peace Academy Collection)$')
page_num_re = re.compile(r'^\d{1,3}$')
text_lines = []
for i in range(body_start, len(lines)):
s = lines[i].strip()
if not s or s == '\x0c':
continue
if slug_re.search(s) or header_re.match(s) or page_num_re.match(s):
continue
text_lines.append(s)
# Join hyphenated breaks
joined = []
i = 0
while i < len(text_lines):
line = text_lines[i]
if line.rstrip().endswith('-') and i + 1 < len(text_lines):
n = text_lines[i+1].lstrip()
if n and n[0].islower():
joined.append(line.rstrip()[:-1] + n)
i += 2
continue
joined.append(line)
i += 1
body = ' '.join(joined)
body = re.sub(r'\s+', ' ', body).strip()
body = body.replace('L iving', 'Living')
return body
def normalize_for_search(s):
"""Normalize text for fuzzy matching."""
s = re.sub(r'\s+', ' ', s).strip().lower()
# Normalize quotes
s = s.replace('\u201c', '"').replace('\u201d', '"')
s = s.replace('\u2018', "'").replace('\u2019', "'")
return s
def find_sentence_in_pdf(sentence, pdf_body):
"""Try to locate sentence in PDF body. Returns (found, matched_text)."""
s_norm = normalize_for_search(sentence)
# Try full sentence
if s_norm in pdf_body.lower():
return True, sentence
# Try first 60 chars
key = s_norm[:60]
if key in pdf_body.lower():
return True, sentence
# Try first 30 chars
key = s_norm[:30]
if key in pdf_body.lower():
return True, sentence
return False, None
def find_word_diff(docx_sentence, pdf_sentence):
"""Find word-level differences between two matched sentences."""
if not pdf_sentence:
return []
dw = re.findall(r'\S+', docx_sentence)
pw = re.findall(r'\S+', pdf_sentence)
diffs = []
for dwi, pwi in zip(dw, pw):
if dwi.lower() != pwi.lower():
diffs.append((dwi, pwi))
if len(dw) != len(pw):
diffs.append((f"[{len(dw)} words]", f"[{len(pw)} words]"))
return diffs
if __name__ == '__main__':
if len(sys.argv) != 3:
sys.exit("Usage: proofread-pdf.py <docx_path> <pdf_path>")
docx_path, pdf_path = sys.argv[1], sys.argv[2]
docx_txt = '/tmp/proofread_docx.txt'
pdf_txt = '/tmp/proofread_pdf.txt'
subprocess.run(['pandoc', docx_path, '-f', 'docx', '-t', 'plain', '--wrap=none', '-o', docx_txt], check=True)
subprocess.run(['pdftotext', '-layout', pdf_path, pdf_txt], check=True)
docx_sentences = extract_docx_en(docx_txt)
pdf_body = extract_pdf_en(pdf_txt)
pdf_normalized = normalize_for_search(pdf_body)
missing = []
found_count = 0
for sentence, length in docx_sentences:
s_norm = normalize_for_search(sentence)
if s_norm in pdf_normalized:
found_count += 1
elif s_norm[:60] in pdf_normalized:
found_count += 1
elif s_norm[:30] in pdf_normalized:
found_count += 1
else:
missing.append(sentence)
print(f"DOCX body sentences: {len(docx_sentences)}")
print(f"Matched in PDF: {found_count}")
print(f"Missing: {len(missing)}")
print()
if missing:
print("=== Sentences from DOCX NOT found in PDF ===")
for i, s in enumerate(missing):
print(f"\n--- Missing #{i+1} ---")
print(s[:200])