import argparse import json import subprocess import sys import shutil from pathlib import Path from PIL import Image def invoke(command, request): process = subprocess.run(command, input=json.dumps(request, ensure_ascii=False) + '\n', capture_output=True, text=True, timeout=180) events = [json.loads(line) for line in process.stdout.splitlines() if line.strip()] errors = [event for event in events if event['event'] == 'error'] if process.returncode or errors: raise RuntimeError(str(errors) + '\n' + process.stderr) results = [event['result'] for event in events if event['event'] == 'result'] if len(results) != 1: raise AssertionError('处理引擎没有返回唯一结果') return results[0] def main(): parser = argparse.ArgumentParser() parser.add_argument('--binary') parser.add_argument('--output', required=True) arguments = parser.parse_args() root = Path(__file__).resolve().parents[3] output = Path(arguments.output).resolve() output.mkdir(parents=True, exist_ok=True) engine = Path(__file__).resolve().parents[1] / 'engine.py' command = [arguments.binary] if arguments.binary else [sys.executable, str(engine)] state = invoke(command, dict(operation='scan', path=str(root / 'fapiao/demo-materials'), workspacePath=str(output / 'materials'), folderName='演示报账材料')) assert len(state['invoices']) == 2 assert len(state['payments']) == 2 assert len(state['photos']) == 1 assert len(state['matches']) == 2 assert {match['category'] for match in state['matches']} == {'餐饮', '住宿'} assert state['directoryPaymentTotal'] == 596.5 assert sum(match['expenseAmount'] for match in state['matches']) == 596.5 assert not state['warnings'] (output / 'state.json').write_text(json.dumps(state, ensure_ascii=False, indent=2)) for operation, extension in [('ppt', 'pptx'), ('expense', 'xlsx')]: destination = output / ('演示报账材料.' + extension) invoke(command, dict(operation=operation, state=state, destination=str(destination), classified=True, purposes={match['id']: match['category'] + '费用' for match in state['matches']}, signatures=['经办人', '财务'])) assert destination.stat().st_size > 1000 pdf_input = output / 'pdf-input' (pdf_input / '发票').mkdir(parents=True, exist_ok=True) (pdf_input / '付款截图').mkdir(parents=True, exist_ok=True) with Image.open(root / 'fapiao/demo-materials/发票/住宿_差旅发票_A001.png') as original: original.convert('RGB').save(pdf_input / '发票/两页发票.pdf', save_all=True, append_images=[Image.new('RGB', (800, 500), 'white')]) (pdf_input / '发票/损坏文件.pdf').write_bytes(b'not a pdf') shutil.copyfile(root / 'fapiao/demo-materials/付款截图/酒店支付记录_B009.png', pdf_input / '付款截图/付款.png') pdf_state = invoke(command, dict(operation='scan', path=str(pdf_input), workspacePath=str(output / 'pdf-materials'))) assert len(pdf_state['invoices']) == 2 assert len(pdf_state['matches']) == 1 assert pdf_state['matches'][0]['expenseAmount'] == 468 assert sum(item['ocr']['status'] == 'failed' for item in pdf_state['invoices']) == 1 invoke(command, dict(operation='ppt', state=pdf_state, destination=str(output / 'PDF首页贴票.pptx'))) print('完整链路通过:5 份材料、2 组自动核对、总额 596.50;PPT、个人报销单、PDF 首页与损坏文件降级均通过。') if __name__ == '__main__': main()