From 3e4df252da4a54c304af4052449a1dc76d789453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=A6=82=E5=A8=81?= Date: Fri, 8 Aug 2025 18:08:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81ocr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- advanced_status.db => advanced_test_status.db | Bin 16384 -> 16384 bytes demo_image_integration.py | 100 ++++++++ src/base_rag/image_processor.py | 225 +++++++++--------- test_files/data_science_feca21d2.txt | 5 - test_files/deep_learning_guide_c8bdda95.docx | Bin 37275 -> 0 bytes test_files/knowledge_e15fef20.txt | 6 - test_files/machine_learning_990eef59.md | 44 ---- test_files/python_basics_db5ddb86.txt | 5 - test_files/python_guide_a94a6076.md | 70 ------ test_files/web_frameworks_b8bf7b11.txt | 5 - test_image_ocr.py | 106 +++++++++ 11 files changed, 312 insertions(+), 254 deletions(-) rename advanced_status.db => advanced_test_status.db (92%) create mode 100644 demo_image_integration.py delete mode 100644 test_files/data_science_feca21d2.txt delete mode 100644 test_files/deep_learning_guide_c8bdda95.docx delete mode 100644 test_files/knowledge_e15fef20.txt delete mode 100644 test_files/machine_learning_990eef59.md delete mode 100644 test_files/python_basics_db5ddb86.txt delete mode 100644 test_files/python_guide_a94a6076.md delete mode 100644 test_files/web_frameworks_b8bf7b11.txt create mode 100644 test_image_ocr.py diff --git a/advanced_status.db b/advanced_test_status.db similarity index 92% rename from advanced_status.db rename to advanced_test_status.db index df177172caa0ec968a60fa395d2d10d8da231bb7..916c98aa17786e39a0251a66d7fd03c9c116360d 100644 GIT binary patch delta 266 zcmZo@U~Fh$oFL7}J5k1&k#}RlYJS!ad^x=DHw!A<=Vhv9nfzNWSl!&p*x1U{OwY)~ z(9+Vt$iT={*T4dZLJZ+DW@eTKlS|}cO&}tsdX}c<#%6|C)f!uxn3yy&GjTFliu$TL zIx)t7@^*cd$rJUpCKu`}Y`(3pDgXdM CS4YYK diff --git a/demo_image_integration.py b/demo_image_integration.py new file mode 100644 index 0000000..f86b886 --- /dev/null +++ b/demo_image_integration.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +演示优化后的图片OCR与RAG系统集成 +""" + +import sys +import os +import asyncio +from pathlib import Path + +# 添加源码路径 +sys.path.append(os.path.join(os.path.dirname(__file__), "src")) + +from base_rag.core import BaseRAG +from base_rag.image_processor import ImageProcessor + + +class DemoRAG(BaseRAG): + """演示RAG实现""" + + async def ingest(self, file_path: str, **kwargs): + """文档导入""" + return await self.process_file_to_vector_store(file_path, **kwargs) + + async def query(self, question: str) -> str: + """查询实现""" + docs = await self.similarity_search_with_rerank(question, k=3) + + if not docs: + return "抱歉,没有找到相关信息。" + + # 简单的结果组织 + context = "\n".join([doc.page_content for doc in docs]) + return f"基于以下内容回答:\n{context}" + + +async def demo_image_ocr_integration(): + """演示图片OCR与RAG系统集成""" + print("🎯 演示:优化后的图片OCR与RAG系统集成") + print("=" * 60) + + # 配置RAG系统,启用图片处理 + image_config = { + "enabled": True, + "type": "local", # 使用本地模式(BLIP + EasyOCR) + "engine": "easyocr" + } + + try: + # 初始化RAG系统 + print("🚀 初始化RAG系统...") + rag = DemoRAG( + persist_directory="./demo_chroma_ocr", + image_config=image_config + ) + + # 检查是否有图片文件需要处理 + image_files = [] + test_dirs = ["./examples/", "./demo_documents/", "./"] + + for test_dir in test_dirs: + if os.path.exists(test_dir): + for file in os.listdir(test_dir): + if file.lower().endswith(('.png', '.jpg', '.jpeg')): + image_files.append(os.path.join(test_dir, file)) + + if image_files: + print(f"\n📷 发现 {len(image_files)} 个图片文件") + + # 处理图片文件 + for img_file in image_files[:2]: # 限制处理数量 + print(f"\n🔍 处理图片: {os.path.basename(img_file)}") + try: + # 直接测试图片处理器 + processor = ImageProcessor(image_config) + result = processor.extract_image_description(img_file) + print(f"📝 OCR结果:\n{result[:200]}...") + + # 这里可以将图片内容添加到向量库 + # await rag.ingest(img_file) + + except Exception as e: + print(f"❌ 处理失败: {e}") + else: + print("⚠️ 未找到测试图片") + + print(f"\n✅ 演示完成!") + print("\n🌟 优化亮点:") + print(" • 使用EasyOCR进行高质量文字识别") + print(" • local模式结合图片描述和OCR文本") + print(" • api模式也会自动加入OCR文本内容") + print(" • basic模式专注于OCR文字提取") + print(" • 所有模式都支持中英文混合识别") + + except Exception as e: + print(f"❌ 演示失败: {e}") + + +if __name__ == "__main__": + asyncio.run(demo_image_ocr_integration()) diff --git a/src/base_rag/image_processor.py b/src/base_rag/image_processor.py index 26fa0f1..d918d76 100644 --- a/src/base_rag/image_processor.py +++ b/src/base_rag/image_processor.py @@ -30,12 +30,16 @@ class ImageProcessor: self.config_type = self.config.get("type", "local") self.model = None self.processor = None + self.ocr_reader = None # EasyOCR读取器 def _load_model(self): - """根据配置加载模型""" + """根据配置加载模型和OCR""" if self.model is not None: return + # 初始化EasyOCR读取器 + self._init_ocr_reader() + if self.config_type == "local": self._load_local_model() elif self.config_type == "api": @@ -45,6 +49,21 @@ class ImageProcessor: else: raise ValueError(f"不支持的图片处理类型: {self.config_type},支持的类型: 'local', 'api', 'basic'") + def _init_ocr_reader(self): + """初始化EasyOCR读取器""" + try: + import easyocr + if self.ocr_reader is None: + print("🔍 正在初始化EasyOCR读取器...") + self.ocr_reader = easyocr.Reader(['en', 'ch_sim']) + print("✅ EasyOCR读取器初始化成功") + except ImportError: + print("⚠️ 未安装EasyOCR,OCR功能将受限: pip install easyocr") + self.ocr_reader = None + except Exception as e: + print(f"⚠️ EasyOCR初始化失败: {e}") + self.ocr_reader = None + def _load_local_model(self): """加载本地模型""" try: @@ -90,6 +109,41 @@ class ImageProcessor: self.basic_mode = True print("✅ 基础模式配置完成") + def _extract_text_with_easyocr(self, image: Image.Image) -> str: + """使用EasyOCR提取图片中的文本""" + try: + if self.ocr_reader is None: + return "" + + # 转换PIL图像为numpy数组 + import numpy as np + img_array = np.array(image) + + # 执行OCR + results = self.ocr_reader.readtext(img_array) + + # 提取文本 + if results: + texts = [result[1] for result in results if result[2] > 0.6] # 置信度>0.6 + combined_text = ' '.join(texts) + + # 清理和格式化文本 + if combined_text: + lines = [line.strip() for line in combined_text.split('\n') if line.strip()] + cleaned_text = ' '.join(lines) + + # 限制文本长度 + if len(cleaned_text) > 300: + cleaned_text = cleaned_text[:300] + "..." + + return cleaned_text + + except Exception as e: + print(f"EasyOCR文本提取失败: {e}") + return "" + + return "" + def extract_image_description(self, image_path: str) -> str: """从图片提取文本描述""" try: @@ -112,24 +166,42 @@ class ImageProcessor: return f"图片文件: {os.path.basename(image_path)} (处理失败)" def _process_with_local_model(self, image: Image.Image) -> str: - """使用本地模型处理图片""" + """使用本地模型处理图片,结合OCR文本""" try: - if self.model is None: - return f"本地模型未加载" + # 首先提取OCR文本 + ocr_text = self._extract_text_with_easyocr(image) - inputs = self.processor(image, return_tensors="pt") - out = self.model.generate(**inputs, max_length=50, num_beams=3) - caption = self.processor.decode(out[0], skip_special_tokens=True) + # 生成图片描述 + description = "" + if self.model is not None: + inputs = self.processor(image, return_tensors="pt") + out = self.model.generate(**inputs, max_length=50, num_beams=3) + caption = self.processor.decode(out[0], skip_special_tokens=True) + description = f"图片描述: {caption}" + else: + description = "本地模型未加载" - return f"图片描述: {caption}" + # 结合OCR文本和图片描述 + if ocr_text: + return f"{description}\n📝 图片中的文字: {ocr_text}" + else: + return description except Exception as e: print(f"本地模型处理失败: {e}") + # 即使模型失败,也尝试返回OCR文本 + ocr_text = self._extract_text_with_easyocr(image) + if ocr_text: + return f"图片内容 (模型处理失败)\n📝 图片中的文字: {ocr_text}" return f"图片内容 (本地模型处理失败)" def _process_with_api(self, image_path: str, image: Image.Image) -> str: - """使用API处理图片""" + """使用API处理图片,结合OCR文本""" try: + # 首先提取OCR文本 + ocr_text = self._extract_text_with_easyocr(image) + + # 调用API生成图片描述 import base64 import io import requests @@ -159,24 +231,36 @@ class ImageProcessor: timeout=30 ) + # 处理API响应 + description = "" if response.status_code == 200: result = response.json() caption = result.get("description", result.get("caption", "API返回格式异常")) - return f"图片描述: {caption}" + description = f"图片描述: {caption}" else: - return f"API调用失败: {response.status_code}" + description = f"API调用失败: {response.status_code}" + + # 结合OCR文本和API描述 + if ocr_text: + return f"{description}\n📝 图片中的文字: {ocr_text}" + else: + return description except Exception as e: print(f"API处理失败: {e}") + # 即使API失败,也尝试返回OCR文本 + ocr_text = self._extract_text_with_easyocr(image) + if ocr_text: + return f"图片内容 (API处理失败)\n📝 图片中的文字: {ocr_text}" return f"图片内容 (API处理失败)" def _basic_image_info(self, image_path: str, image: Image.Image) -> str: - """基础图片信息提取 - 增强版本,包含OCR文本提取""" + """基础图片信息提取 - 主要使用EasyOCR""" filename = os.path.basename(image_path) width, height = image.size - # 尝试OCR文本提取 - ocr_text = self._extract_text_from_image(image) + # 使用EasyOCR提取文本 + ocr_text = self._extract_text_with_easyocr(image) # 基于文件名推测内容类型 name_lower = filename.lower() @@ -191,75 +275,20 @@ class ImageProcessor: else: content_type = "技术文档图片" - # 构建完整的图片描述 + # 构建图片描述 description = f"图片文件: {filename} | 尺寸: {width}x{height} | 类型: {content_type}" # 如果提取到文本,添加到描述中 if ocr_text: - description += f"\n📝 图片中的文本内容: {ocr_text}" + description += f"\n📝 图片中的文字: {ocr_text}" + else: + description += "\n📝 未检测到文字内容" return description - - def _extract_text_from_image(self, image: Image.Image) -> str: - """从图片中提取文本内容 (OCR)""" - try: - # 尝试使用pytesseract进行OCR - import pytesseract - - # 提取文本 - text = pytesseract.image_to_string(image, lang='eng+chi_sim') - - # 清理和格式化文本 - if text: - # 移除多余的空白字符 - lines = [line.strip() for line in text.split('\n') if line.strip()] - cleaned_text = ' '.join(lines) - - # 限制文本长度 - if len(cleaned_text) > 200: - cleaned_text = cleaned_text[:200] + "..." - - return cleaned_text - - except ImportError: - # 如果没有安装pytesseract,尝试使用easyocr - try: - import easyocr - - # 创建OCR读取器(支持中英文) - if not hasattr(self, '_ocr_reader'): - self._ocr_reader = easyocr.Reader(['en', 'ch_sim']) - - # 转换PIL图像为numpy数组 - import numpy as np - img_array = np.array(image) - - # 执行OCR - results = self._ocr_reader.readtext(img_array) - - # 提取文本 - if results: - texts = [result[1] for result in results if result[2] > 0.5] # 置信度>0.5 - combined_text = ' '.join(texts) - - # 限制文本长度 - if len(combined_text) > 200: - combined_text = combined_text[:200] + "..." - - return combined_text - - except ImportError: - # 如果都没有安装OCR库,返回提示 - return "(需要安装pytesseract或easyocr进行文字识别)" - except Exception as e: - print(f"OCR文本提取失败: {e}") - return "(文字识别失败)" - - return "" def extract_images_from_docx(docx_path: str, image_processor: ImageProcessor = None) -> List[Tuple[str, str]]: - """从DOCX文件中提取图片并生成描述""" + """从DOCX文件中提取图片并进行文字识别和内容分析""" try: from docx import Document @@ -279,7 +308,7 @@ def extract_images_from_docx(docx_path: str, image_processor: ImageProcessor = N with open(temp_path, 'wb') as f: f.write(image_data) - # 生成描述 + # 进行文字识别和内容分析 description = processor.extract_image_description(temp_path) images_info.append((temp_path, description)) @@ -295,7 +324,7 @@ def extract_images_from_docx(docx_path: str, image_processor: ImageProcessor = N def extract_images_from_pdf(pdf_path: str, image_processor: ImageProcessor = None) -> List[Tuple[str, str]]: - """从PDF文件中提取图片并生成描述""" + """从PDF文件中提取图片并进行文字识别和内容分析""" try: import fitz # PyMuPDF @@ -318,49 +347,7 @@ def extract_images_from_pdf(pdf_path: str, image_processor: ImageProcessor = Non temp_path = f"/tmp/{img_filename}" pix.save(temp_path) - # 生成描述 - description = processor.extract_image_description(temp_path) - images_info.append((temp_path, f"PDF第{page_num+1}页: {description}")) - - # 清理临时文件 - if os.path.exists(temp_path): - os.remove(temp_path) - - pix = None - - doc.close() - return images_info - - except Exception as e: - print(f"PDF图片提取失败: {e}") - return [] - - -def extract_images_from_pdf(pdf_path: str, image_processor: 'ImageProcessor' = None) -> List[Tuple[str, str]]: - """从PDF文件中提取图片并生成描述""" - try: - import fitz # PyMuPDF - - doc = fitz.open(pdf_path) - images_info = [] - - # 使用传入的处理器或创建新的 - processor = image_processor or ImageProcessor() - - for page_num in range(len(doc)): - page = doc[page_num] - image_list = page.get_images() - - for img_index, img in enumerate(image_list): - xref = img[0] - pix = fitz.Pixmap(doc, xref) - - if pix.n - pix.alpha < 4: # RGB或灰度图 - img_filename = f"pdf_page_{page_num+1}_img_{img_index+1}.png" - temp_path = f"/tmp/{img_filename}" - pix.save(temp_path) - - # 生成描述 + # 进行文字识别和内容分析 description = processor.extract_image_description(temp_path) images_info.append((temp_path, f"PDF第{page_num+1}页: {description}")) diff --git a/test_files/data_science_feca21d2.txt b/test_files/data_science_feca21d2.txt deleted file mode 100644 index 1cc6f29..0000000 --- a/test_files/data_science_feca21d2.txt +++ /dev/null @@ -1,5 +0,0 @@ - -NumPy是Python中用于科学计算的基础库,提供多维数组对象。 -Pandas是强大的数据分析和处理库,提供DataFrame数据结构。 -Matplotlib是Python的绘图库,用于创建静态、动态和交互式图表。 -Scikit-learn是机器学习库,提供各种算法和工具。 diff --git a/test_files/deep_learning_guide_c8bdda95.docx b/test_files/deep_learning_guide_c8bdda95.docx deleted file mode 100644 index d0232ed8b9463159224029a4c2ed1c1c47c70e9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37275 zcmagFWmsIvwm*yqcL@#wg1fuB2AAOO?$Ede4Fq?0cXtRL9D+N+t#SExX3m^DbI!f* z`(^V`wboBoRaaH-y<15Z5(*Ox3=9@b(*stgQn~PJA~+b>C=3`F2B=j>)ZWg;)Xqg; z)ziV$S(m}X)}}F0UU5YTHT3)iD~VBn#9I^vt8CkW%7N}Ho>+A{kV|Wc@l+1t;clEK zLTy?p42I!rM(Uj}(MlbEmrv729Z|7UUEK_g{11@DKKW~T3}@kF3k2wqoTczZWXe7YHuC!j#to zQ~Jj!dVZ7@x(AKkIs_OP0_d;4lc|j}Bg5}&W&DH;Br~f2xq$e0iekGaHL-$aJ+VXi z0)d{C@v|a3fK<`XuFg+lx|-O%L* zNpKwjq#ml=Z=#*bF90rLhs_1!;-@r_Cpbv1FH}Dltx9mvn)02};Xn0MUxvmF&@^hFX zI$^3!KG$xHUGjw;nbzSr|7U^1LlIOgLDPI1R3J1^flTa;m7MGyoEeSnolJjE^33=# zg?=Vf@jD;!IeGPnYgAZqX&UgOFEUb?0ZXm7Z0zzjV_6&%8#@KI+BvjtL>s)r*A4<} zoQ=(p#v$<`#9?3015CakXkops7-6qIWr@>pS0)Dco%cM>UVSv@F&^!S{Dg-HUy}tX zozAQ)w-C-lHc-dTHWxZcn%SaUlzS+mf+oKlsxA~y)kcOyvWH3iDu~HLU8K#0F8(O&MNdNRoT5oon`aOsk4*g#*`bRZaHFe7$R~KZtnDH zNrg~gF6^p~+q(TgVeRnT-1aKHGljpPi%AP>4aQ5;7#C%-As}Nd7kSv7-#iR1^V{9c zr6qIvH)+gb;S4D0HmMo9u8F+EdcQZxSKK86+I!ARsP{+a$a>VX?xL^HeOLJZ9G9Dw z6`W(xxY$5}fua6=Tnrr?{umb3F}oEe)Q%@wC|#E?mhXf=y^jxE5S|uY!>ftUXr3GW zAoCTfm3bTB`w))B9m)ud;H~Fn@wHz*i zBTs_QY|wczZUJS5;OzpeGx>#KNB@TVKi> zhb()G4E4w>vh<8CM)Onjoi<@yLQ^V+U+t*<^$8~zVnXE=q$c>6tKFyl%o>buLY4%} zZD{1alhXp?wPQ8qGTRIlr2JJwl;lZ-Pe#P}w8qqD8V3iWi`u#hI?-6!?09Hy6k8#H zR`5N0O6YIa{O^?{-L$uf?=0|MP#G?5nGZG}T=@}>++nx?E%k6j$)|8OFC$T7L;)=} z8_9_R6B--jykQwQYTh=AUy}z`qy-BVB|AIP8J`)6F5uE1uJPaIfhWZOoER#G7g^n) zNfCkk=Lgo^-pPa!^r>~V1#M2hXGW!ljtZU@Dqurj_|oWB7lpXn=(#Fqs%*GIfjE&(PZpY=og!%rlkSSK)3^ z|Bidsx9IIMo4^{km?9O;LnEv+^04LyAB6|*`uWklu}w{Vb^ixi@^cEUfMQ)jjE2?A zr591Z@4v80NF=YnK%;srj6~;3-@InVv_*i6 zUtEx1f-@$Ufe;^KX~8FXa8zzm-{RV`vmlV34_$+re#g?k$ox7bg=$HJ?Hf4o6+S$k zn64mMxE6y+T-lHhaf}LsCn+D9^{fttVqI(Su;TiMZH7<}_hc=*=@y4>f(1~mKJH9p zhRsu1zEybY<1`&Awg-1lIHzYjBOV=$6-hJ9DyofC!<~T5Y_^mMtRAPod+s@VG}nR& zfHyC*r9b8n)ittR>3+dHT6$*-VU3YJdITs)*DM;3ifR8NB`lYJPk*5{2elzkZ7qN3 z$8OkHT%@=UK0*Y0K1T=85jQnGuq<}diUspX^CM&)lsC;5d=lh4>}7H_4HUZslv>HI zQJ*w_S~=gX9KjEXI_}hysjiB*u3bIAWFhUi5KRp$8EGx5BKnRrl<``awZsqq`rwt! zF7C>%%n&>l5+p;Hd)g&(Nu@XH*O?+2rm{C??MfZlq%l2R&{kiS6o0fAs4cT|$E#Mt zneDKRirO$y(Wd*1p7kt{P?YToqq`G)@-QITw{bPf&m*^oneD)kxTmInQ1t_egT_9za%2lFBU3jJiWe^~Pss$DCS zhG%k%{$zXle#d93Uecc9Vh{eA4`-|r>Hvf~x=J>2i3nC##*Gw>vHXCoE#j4vy(1yt z7h*=npJ#gA^qciIr5NCTGF`uJ4`}rPOt9Eu};ZqSCXsc|X??eGV*6 z;pHhBeJ}^#0;W_mml^e6qXmO6M+n0Aj6OwTj?Z(jMi7>KRiV$Gtut&O{G{@U#CSm) z*jBKI)58nE@BbCM-*mJ+#};F9lh>YQ#hU6<70=Dl4)ud+&`UuMt#Df?zY}nIT#+PI zI!w{{4ZQn@rh^B{L^u0EFZ&mT{C;N@8}HX?o2EKHwPiu*MJ3oD3z!8O6DNnH*?ZG! zVk?n3_eMG^s*PxjoA)xZwq;j<^3V4+H~RU9c(7*mEcoTO+&U2yK$ZA4v14bd)Wur? zRipYiC4QovRclJL&vI%rhCXa?^iagr!c~o>vTW@|`(|B>Urp1LCCFh>0M#9gpVb53 z6F5B2uv%2gM@v1DZ;=r2LpF|lVq!7#_9Ao(b4*2h$@lA}S(Q6oTk8z2!u7vY^kr&F zg`bjMi~Z08{d3*AOe#tcOzKvk50yVK4|a8eMfM3#9Wu@vZ|Ql}He5hh2~W&ViFu%m zSH{3JB~d&+PoB^i?-e$Te5l-*m5;xV3bW?K5q18Bt==moj*Q0 z3uqf01Fd)jpl$Gv)$Y$j+~3!{KM!=@gcXCB-iMyQ!uN$FU>4a$;-2JvY^RZ^F7yeJ z_!)mIy0@XTWr9k6r9;hRlqJ3Dc!vartkp0+_IkShuT1#>n;$rPQ>93}a`TCb_#>-^MfD2dlLe` zmv?&bW)1TMqD6ZF&+?z6(e)jM|aE?jKRd*yG4^lSqf`1RJI_midiFt zxhU$=RD!KTa3`#}tZt^i4R|TQxp=>W5Zn=nDkj+GCe*b4G^QbW%-?sBf*wFZX^ZXr zvqXL0U{#ju=go9A6!$~W%zxDS-CT95Gb0!nx*9YX%3rm1cJZ_^b^g7tfWE9GY)jPn zc6aCH>p$<>#sz~7PuhXSyAc?AcV^X|v&ys1p8WWMV-b6*??*u@M69N)rY=(!U&q^* zg{?%rr;(YUYWnh$?bl&*4$vo#Nq;$8T1w12P|g|du!RbEgFU@80I8T z9MuS(31+@x3&=T1UMg)3hXvj1Gp;;=M7eMHt)HTd4WB-)`Evbu5fqu=Kh;a35N_LNFVBwM$9AmhAKuF#z4KTOHtx(mjY)sHNCaMaq!DtC-9!SM z=)GRJy~wT;%6F(;J-a-At_q5Y#QKFWzILs4wKrFcB@QzC*|x9xc74`4?|K1>KAI^f z6ZJc8tTCc&08+$>eWbEZN#6#lAG(cttHKw5pg#)DA0vrXXrdrVf83s)m4HoHTu34V-)}`#oAQ_gUY?YueD--vr5gSzq42b!LQ~QL^0$km?ZiY`S2Pxt8R39P&XH@}t zn`DXER4rnAl<-7Kpr&40!A~{zp#S#iUIwgi-*oTCrIy|a*eR^InnK{w6t$6`ZoG8x z(FqbKRZqE3uWH9Bv-E{E4<{^DBG20a`+wDJ_*P*WYgrjRCodlCIXYKh7nD54qFuB; zEgD{{QdK04evz6?v>UJIU{3uJtfXw;pbyXfD)zW#OA@0`I# z8`4Tps1g0vzwc2-lyOa^7KE_;WAC2A?}B(yF}*oC(hBNFxHavUnlf=NA+)dHVnl`o zEL}Y>?LC)d_>rq`nx;MhPuV3=_8PH_nU)=K&0j(mHF?}{o=#sce%aDy5Dm?s`CyUW z!p;c$5b9G&M-`HBB?z3Gm8*SU6?;}Cjg^u5Bvpd0{Ef9dm~Ib~mQ7P{FBXPcJoUqUW)b?y4R;Q%TC^^^MQipFEz}p8`4Xfw%ZA) z1ssqvu*&K16i~efVveLo3HiZvjategaX($CNny>`F?;l3uZ#RKgFRJ*;D2^dlOWix zS7hc(!77u{q6a#}H~W)h?CSEyenlL?G{q_79q;u_HrLRGlJMh#Brfc{P|a)lii z7}o(^kn_9K0@R89S0~gKxjne>zv8PJ@hBLe7(Efb_erlq{DpnW77t1#0mV`Ok*Wtv`OxNN&FwEIIpWJ?7(DD)Zc}<;e+%j zqk_C*Htf=qAkhDZM0yffN>2pXKUB{pxHqD9{Eqr}Q-QN+!=zh#7lvLIkC^s6Nf(j2 ztMB;(xSy7vHt|AxVvh{GctA4cx44p)CXcQSzg7&LC0l0#G0nMs$M)snF`HMqD1HW= zr04U&wI`r!tFGOkxA1fS;<#I`#gfOh&i&AAcJ-D=^{;sW#e=>5k-V+@=nX;LW}p~x zjNLZFvQXmZm~-~8cyCV*h@$kKQwFGXtEGhGcjzcuju;xX)5cXO(6v(sW3?s2rX_cN z=bG*fYbRZwtFk4v`4|&o*BabBhuN0t;~J@U9DUrmQr{hLQa*Jmj13!Ky^THPv0-dlF+b8(u~;n_ zaz)JG_l%nMdLIW&7zP%F03V|{Z=`7+Tn_LgJ=6Qj3l z2Q%q&)r8B{=CgwM?D2ZO{VY}v3#oTD<0je5<~DJk&n$rW!VeSLJcb4pWA;nTx^!YN zVJvZOe_Q%$H|$D@8b}^_ER9`6sRfZW@Le0otx?C>Hrg60!Ig|57wQ~)@Yz$t3AbwZ zLreUGD+T^+qK&BT$WbUQHBUspEBd=xX##M{Ry2BB-=T$hbB=?zrY&Kzs&~;${1$Ua zC{8z*IZ?@)F@zc2!l*r>J7B%`pQ|=nQUE({{q_=%_wv$Jmg|v2pBeFty{236^>*>L z#3xA}C%mVr=LRRj1nlw{#GDav(j0Ux0)L4l*tPu7B*szR{0s?mfJbsa^X*txMNU7> ztv~tW<2||&PoGnRoOI%KB4Yi=Gb}YipGdNKl=e2_`Y*&}NJ!9;#=#@ChU3q(pqr|7RgHXILX-V=)f~SnSk?%QO zJWE&k3!Rc-Hga^!?;a=teh+zIJJt>N<>6PB0o8QAWw>psM-)gliFivQEH(4&keurD z$!x{CRtoqd8E)s}a3MTqFwM-H z*33jf3y+azbG;eY(*rKEI!Yu1l^zz#H|hF6fm?GSR%~tV2S#CezUHN&)d7axG58Y# zht$o6{lrS`*RHQ5CfFCtWuV2amyt#`r z<`;g5XR}e!|LRy*Ry+Bqcnc?c6S{}^j4mv{uJqZ;&AaSq(5ym|N?%RgcvJtNZ3U6p zWR4*8JA2rPbFu}-nB=`{7(B7CI;tPM>12lggYuxx%xsab-ttAqs&7n8Qk&w!!j3}z zG%L2V^$bVnka;sbX9}_msVLc$-Z=SXy$0pi$1Gp26t*iCpiQA|4-A1!{hr+-u z!LIHu_F?Gb8AwXRNk$GLBpWr&iIPAJtO%MkWaK8AlQy+Wm1llR9gGocowTLZ;BytSx7o`Pg;(MC{3>E0(lSI_ zCJr!3`0z8H^%Y(`%8JOl(5;KBd6+xGov}sI4tLGlo>GtD;93OkYvdJNwbfFL$tr08 zpP=jDBEl!Z*yjFyxw4(Wt5X>fv1%|RYj8a{_o+6Kb-!(mPgcI1#EE%lmK#4s=!i<`B1iYb`H*M4h{EtQq<2jgm5&}0S9Tt{qiqFrp z+lu7t}M;BDOLYP%E|zSf`5t$f!{w^T7FuSz)^C93Wbrh36yHaRY` zf0~vqbmjY1?fN@vs}(1%T)ZifnzKOor#;`O@?sG|^Sxl&Mra{c_cox<{{8GJQkhpB zsTWUZs~R zDC+ar`3rO`ha_osYdqcC*Tf29&e=Pq#~YChQbJM`M=>Sk6(|S?h9>AlWIOi#a?qNk zxCid<8JRsD^}A-cxD3hdCBG~_INjYCUN5Q%_FMZ5ox_G5bVHK$Q4*L}-eYU{?6eQ$ ze1sXAP!%MuB7SV(CQ!_EJWs9iAYEJ5(+2A{Neqq0=WXwyX{{@p$Cy#+v8%6~;CG^a zqoGM2b6)ft)Ajp)xU-d47}h|_ZMMcRQnpraMNqM&yQZ-uG$$Gt>V-J_deIEr>Ts&< zdzLfaI%aP;zjp1@wOt$&C0Y;m1N^ix&ck`pt0Wr0sjoM2IsNS2N<>0#*Qm*Veizp5 zRZT?2_T-lI%~^)@=bjS#Z2)DOF}qMKGwXC(&!Sn}9KA9O`9RUyD#Ip{MSZ(JortZ8 zmWbVu5XRIk#Kx3x!gI#5`AKLQiTKUXPl#b5`;$zEwEX5$YXsL-|cvW+{kj zf}|hmG0q+SoH-Mh5OuJfUrpgUaUm6+NrEYxmzj|i>NK9dtSey^gs*Gz0&x$cI}P=n zZ#dkFlR6D3N!n!(ZF#b%Bvsaf0x>-HYJ%5YGIpRlz-?o2;=~W5>0}Ql2pqN_yEu&k zF?gLDn-7sxgufhCdJJ)3bk?G*E)aA^Lzweq)nfdpBwqx-;lOQ~9N*@r;tJ3}mOYdo z=GN=M|FQ1@t`)rgy)Jk?3Y=1JQ~ntaBZDio+*jPb1YTLpo{ag1g9WW~xj%w6vmWCI z5}!L*C-?QT(bg3aW=0Qf7{yJ|4gupPl1F)dnJJnV*%-PP>$?oiSGw;En>3TDof7sb zzhSE0dpXOC*>{~Xz-0Vfp=Wdj>xAAZJ$7KJL{QvJ>__*Sb&SEefN9{?6T;5rtfR~2 zH13h!e27WjP)f~aeTo8=h&+0o>z$y)<0jD?6~ru!V$0BZc|LDh{LkfTd)Bxa7{NJ% z0>{l{GHT)HgvFa3C*a<-;TyJ@3=I89f$Y{mV!fqTd4~-=C!{L>=?0N~)tPdNXkW+h zW|SEs{by--#`uY}CS2G)^&7c}DJ@mI6pC?H9fP-#vb%{oue(?H)nUJ0$y-i!V z*Y9+LL)N1|(>wo_uJ>2Eq~h=N8`LAU)B0TZ1fsY_mE&xNy!8yV`byfzC@<>G5Qr=# z#tBld7S+y@WScM(UaYyXW7}^MofktA!Cq=l35RQ|u)DxHmeR-)PV;%2u~>n8J$R2? zf66&o=WrqhHO3=~71Ct|XyBEXNZ}{O4pPiqz4}82O-CuYl-f6;1z9OB!?nlU>NYH@a4F%nrj5P`6R4N?tJ*73vuO$C9 zl!pe~kz2?U7Ice*8E?1cr;UIFjj#J#-SheD1pqsZ63>*-C~DcUUatF)axrf)JD-z9 zu$3oEIr$*HSUyim9MjrN48;}MQV)Nssa=^}v^**b;;4NwnZ$ru!uB(^luBdk+nFLC zUPuPbaA8)w?TTDpSKE8B?nTGhOo!RUv|kPiM1~NqvkOynSM_#}xK@=_c^g(Y*g!zJ zYDYf6I_{IOINz2dh4Ht?er34+-NSwCYZo~CMs11AO3u;($%#;Eg1YOVszGL&vriZ7 zxQKJgIaCLihlZ=Me%fAP5EDYn7F3Py*0@rO=V;RFPLED3=u z<$qH^$;%5HnVz7BI6_~yQj6t5G0XjsV}zzO?2+S;^C2hnZXCvf7I!-xN2 zLslvHC)-qM{{I)7nD7unM}%N&Oa{LsudmuH$dN?3s-kgrc{Qw*Kyk#>{3od0%=ruk zh`uSSq7Md+2wa)-n*1~slsShjb?LXiY%FQK-V>}nXZfcEHydFz;KiO6e#o&>6hhhw z=nMGbkGJk8_>pWPP;uo0$+E(X8w)he(p&c!*>zb&{~dtQ`^uM~Dk;s30r znDEC7pJDvZk_tE`tK1y4cChbUl+j^K0pdQ(*-6DlKT;n1ja46y@AAt zS=W#mGxb!=%r|6wyft(ggYjwH==(I1gNqUKBT>q#62$DucRhM7xyxNmqR4=3Vp+PP zIHZGHBjzm^_=f2s)Nsu2m=IqfQ$$FNgy(#bYm zwx)~>IH;-Lg8jocy$sCv06H0%@PO0Euh?RAMYMFGAL=u|SJ>)<-YIcwvh@0!N>ehosH15ED&THNm3i{RAFAU8nd?!@E8>pmch%pGF zuc0uD1v$GPTj6+;ElVU>AOF4T!Hc8tgESW7T$B!G)UZ`OV5DJP{f61dDkR0Afq z20`}Q8Ja_sla?7HKXwwi%<0Q0K>hVABtcxofHsZ&j6_Lr%_B*0)C2P0?bg)NMs*xD zguIRlbUPRaY?CI3mogdk*z@S1{$<-Z;S3eQHCpG%(I9*}U=V(|^B?Vv-^lADMmqK% z2DodZ6fZyDhB6v4g^~NHe??xCq z#DOnoE>X{Cf9%jxZ)dNj4;wFAFLY6TQrkSiQuRA#seoz4D)DMTNqZB3lPg~#!xX0? zzA2kdf^Psf|HN(C^bV6lk#VdmF}8x!BE8i(cU3KfaUtzUvOl9T z1^Uq;fiW<$&mGFS3-%%)lj@!>y`(yk^ z(6UPQYDh^A>9;0^a2>`dz397*6wA7AshR}6q8(0}ffS+r=%fhl_(=`8yuP6nFh_(a zNq?NOCtB9>&Hf^<76p-~^Pv7g9-Rv!-}*m&Lrl4MJntv{iyR9^b~L%IPot$RIxh~p zC_z9ddoXfU5xwXX3|(vQf^ z;~f=mw8U4^PN+&;xyD9jvbpiZ-)%9r2w$#rrfPB$R`U<=qs zzH-*rAtV#ZDc*YUXN=!(kerP0Aj@nOYIl< z4{0!YmV2G5^gz8bu~{CWS*SEp2<+$PIYYFj+5E*$Go-&fwsFcc{0%I9BmE=qZi-yYsPUtJh@Pk;$Rl5fN@@P) z0TFI4T>yiF3hLQI0H3afp1k%4xEMSXvCu9UDufs!ELjh}_yHc1KE zCR2%o5MdcXXxq@g(GaZofAB=%fY3Yw>NzbqvReD}7C!0;b*iy<^wfji%!@_5!P#NBT;(ZtX3%tON0}X$pngK}%CAOFxwuIS{V^F5LP04!sum@R<|E1c_ z9gg*GRh%w7HUBG`{I8~lRGxZL-u!iClkJ5-Isg1X!hAd5<_(8;QU}3J?K(#*YY`i+ z{u|uACR_(+_0TMQ0m8zko@HSnH#?<7SPTA=E}xfUc-3}pgO3|)T)U8(4xrV0^x`Ca zV(*DA`E8?wd{71%)>nxg)iHk!-T$;N&z*cw{RHpvxh80?+I2Qne-Ov(C|WlB!PWnP z9wtAaKSBv%^ce~Unq9e+2z!RJ&`VV_9dp{zv^`HXdIe*DQ3T@~c8WgJCI1EnK*k6F zOUdH&!uM5$biQpFe1%W{pRsRppor_#$xV9X6pXs zBS7qUTeqULPsv18GAItLiHpIW&+o(243D8*sm`-n?PBWQU(sspeU>1-GuGhJQj{K_ zf^v`@uMUzR*$*>HE(!A=W^iSm^@<#?eo=Qn8dteHO!WYnJz2>s7vA(&YY=0p?UJW_U z_+C*r3?rA}+XEaTycL%oKV3n$2e72ZZ98vg)D5K?ubFbZI>c+iPGEM| zy8Y<4Mf`)}!0b|J!5e~-Y8<6rl=&sq3s%?1PiH*^i=;`rI+gVgXIRflzMm^MHEP{8 zUThR-?mvCW3DjRi0N7^umm;kH7Q7k8RC+L(IyMA`_544ApXKQ9{}c=l00{>1%*Hyc zX;#-qjx=kVU$4>L=RUS*_f|hknd!Vpnf(iR7y(d8;8XDz@Ju0@bB6^7Ovz?(^<6s4 z9&qTPbkp3b7lr@b(J{2@g(y1**0WqDKy^*3p6vJ^!1m1+!D;S)9lsRGd3HJiXzo+BFGQyT9=*UpuA}9l1yzcP z_lN*w_Ph1RLv&|C(A~(7>LBkW0#)i4&V%p(2CQe0G<#4)!* zlZiQoRdPphIvsGjA?Qw`-%ViN#xHAnezdRQSn7qCB6%I*zIceU+>=4Z+ilaMlZ_A^ z&sbvHMQ01L2?6J!C_7T_@$eE_L+>oE_Vv&cs1!z}5cNEUgKYZfh*Va{y??bYrJ|@x zR8nSe9Rz8BM8&BZ8)H|7E)?=uDFeH$(4|j&_4RJA9Vlyo0YwQL<0!!p-~h3u3$>MG z{Eorv6m=jE5S2f*KB)N+#t}aOjA1SiM}!9ki>nuXp04M#Ui`fYlsjGjOf z467AE5HJ!ADf#bg2EB)kxr2*AxHF^pmjb+~k7p?uU{J$4n!ExTT(hk$!mu&;_OY)$ zmqp)}5x8Q?<)FFrY!W|%vN=Znq~|S0`;%UPkaq{15oDedW!QH`7jcQ5vFN|jak+?w z&x@NqHkrUNf86f@+sE*8G@k?Iw0pL>h2je;*gjaoh0wbCO`RsnWzjTh)zNic!&EA% zaa%#7XtT@*4b$0%ewn#XpG8!&JP8ZbcZn~5u)7&6fc<5CFjU!JYI!41VYYHAkOiEs z*VTOSB87@sw$p31fG`Li4MSo7!E4KtxWIlK9A-ABL%gY+jmUt*>Sf!?Ow`j7<%}=a zY>NP-w4_4%;JJ;JelO{O=Mr!r_zjgRB5`=SdNUg0NT%I2n6_n-9(FT?+_zFV9%5_T ztzjI45jd1I&?9GH&>HhCRTzsarKbiA`y&(0B~zwn3XUeYw;wGtvyl;@snJ58R@8DP zPk3ahQ*DNmvr6BdQ>RS@I3xz~>UR6bf-4vxKA z77sraBh!yHETH|eLJm75FIuKT&R88=&S&btwU^$^&B%0Ml&5EO$k^JjcCm-)i4vVQ zL8_X2Zy(~=sZ~#X!9O4(@s|k_oBDq2ege05KB={cO+un)uMM9uW1ra?O)ZP3y84Oz zn6c>vNAtjFoR(IVyP6q+_GPtqRQ=+LE9*x5_!m`>C5X!4H&u`p9X4dFZx`-wo4w;f4?m1C zL>DS&?qseiDwc55hDSG%@*)yR>+3!*HjhK?ih%sqJDdGLqXp(6AuI=}@q;Q7t(AHX zZP$|(Tvy4(J3I{(N zHJ_)hm(%05FpSE*Yb4gcnl zr=D{Bgwq04**Z0NBhxiQQ)l;GFV`8dh$*c$%8Vzhc7+=n0*2FMM zub@j|G|OEseN|wt~&1af#09`jXjF+PuaQSG?mnzq7YW}VfE9g#nUvIPe zVvPNy^}#%|ij4ML#BJ&mp6>PN4^I0u-8>8Iq~vtr;~%~Vv4L**3YxFb!Aw^h6xCg# zG=0_T!F&y0FVS_&SCUg&>*4ZEBJ1#G4hRPL2k-OFpv#e@2r&g11U88CuOM&{g1bP% zgO0t>qL(_A7&PBp=H*18J!Itr;XD1|=^LcUON5jHav+4EA0YLDQ0Z#A;|=!8IC4b{ zJe}$<){@P-5T7!khNjiMo|NTxX=hSn^?(1Gx6 zLePtMonIZQtT1Tq>kSTTao=C79|yxz@F&hFfv+n?H z((Kw%L1MxCzK5ehRs8`);}O8Qb@I)4uS`9+?>hq56Lm_nHl-?+<;~Yn_zMPNC@j#< zs(rZngR=xx)7ZO5pfI>qq|2=&u08iko&X99mgW!wt0KGt;h;=?k!IXSux8M{QK3Ys z?uicxYg1P;WZSOfeQ5A|d2&lgo7@h1%+?|cHfYZ`pxI8*?SVz&qr;O) zi9R63#kuX{a0GovrTGvN-x+6F0Jec3Efo+ak|`x@+pOd*)k{1kdIZ_co44 z8UlJY&Vi53++2khjfv->*DA4Qd8n*lYfv}^+4_aHi4b!E-f$CUaMMf09vnl(Z2a^{FN#&{+ge>#Bn=5j1LsUA#8{}d$w530mEPsmTjllf~0L-7KoIuw>@%*`hf zIBHbo5)A5+Q%oyRp5I9p0OY`Y0hao{sXgwOgCU!;#ik(|N=L;LrZhiFM+a%hSa;0Z zz28573t`|HILplr2IgE11BUm{_wzL@T`a`S%uJ14{(M#6S2sCdqVDwR0ju$7P%$eB zjC5qocM=_9NN;TCGWGUepuBueH?1oENQR~v^tQfGwm6oDR_9^l z@cUQ4R}aswC*Rh!N{-{Z4a7H&`+d7sz!G28LiUE=^|}6zJ>|yg>1`x%XWO%0uj_4p zYVR!l?cx0G@%7lf{Uv*9Y)$v-33%3CU5#>no_z~?g@3(sg`baJAG^J~vx2B8eQkgC z)gC21**#q`TODQ0U7vGh*nQG~i|e_2@pRv?b%kPRKN5Hzx%N`pnvA4x%@`SW{pJc2 zBz<*f|x7t}5^DXFpU}z0E2l#mk=y#O1-FoB~zj41AjX5U_X9uT;H7Df0+GT)74$6LAx)yw>p( z@)Ge7^S!w~^R8FiwYOJq(7U0O&sUGUP*iqTpo@tpR|0Ms)pjYqyl{1xy8u4hW^={(j7)Q~3mk*6PNFVsUX0aS~%Wv3hLm81U(7>!A-gd*t}=mX&nucb*ixxbucjy#H1@ z7&fuu<>!+l`;PydC|{5$srpx5#^*Y{g8pac$MS<4%J!&QQGEOIV|qdqOgUoY*SGq$ zt=9ZeIevPaUsb-G6^$Ge4f{;^IQGL7R>Ndg=3;X(igS}GC7E!acvS}7ZqvialvOA3*_yXtmo z>?hDxsx~9*Hnm+Jdu8z8bQ}1KVGo+K=qT;s;?{A>eO5Fu{k3MpPWe{Hc+3&jMx3!) zF>=jH!#>)G3m~txzP8a^KCJDXAOBK0)W`C`X0oiN1TRd;4Z=buH+d}7nRHYBGhKKN`DNwbpH_1QP`_(78 zFGj_FSfw@r8Gfoi>RYq}UqrPLIiXJ$B`z)gmPdR<9a{5x_$mz+LzDjf{!j~5G7z3=kak)L7 z@wEE99azsCS>Li(4)1Ux6uXb1;Jn^@JiwkhdH;G@&FsSB%WPkN`o&Q7uBqMROI8&T z;&!W`>&`I}QPkYV@}8aoKaqp(73=$J|uBL=Gn4# z;ls(Zqz_u^Gtjiv=Y_kq+dafsuySEc_jVK5KN35pm~K1cyT0o`IqUu{ znf=TGxNn;c5PO(CpGxL!T@^Va8(m$j+(`CaX}eb(Y_9w@GGnjf+}#0KNh6x{TS<#C zK8y7{yCj}@*?gv}68uv94vP3E?+MoVIZl}Twinmh`&n+>gD4Y$>`Y7r6M?Ku6Yt|W zQFsHlvfdL5sCqJ}{pUw>urFbgqX4O3Q)Rsz@v;9(&yS+%wrLn)^RN*JoDT^V-V&3rc>hDhQyXcoRDw$J33)H#{}J{Uz;P^D zm#~(?sLyMS(SY= zt1|D4;%XZjPDVU>dqjulWwwl1wYy%0F^|0tU+%`_#k+ zJ_`E+0`ZKRqthAgQ}vCS+_{HY%ogxB3fK#yW(97=o+-jMZbjD1`u>Ca+g2Gzw{T=k z2`0>YQ}gYIH;J!vLK`OC&Q*3=9#;wM$@oI6p+2xaf5Vyje?hbeExx{T@LzKz;)EVvQ2RQzGZdAMZy2Cum+2uG_0`A#nlwE}~nG9AZ7r zzO2BYv_4iJfAcu)VKwEE2Yq)9c%%@1ZP$6dZXxbP(%F5FIw#wsM|HyauiJIQzSO2< zXXipusk4DhGm4wLCLMa1Dg*IjmFM9ISfoyKkeOOkH+KDcGF<-XIw2-w!Y{aZK2hkE0rA+ z)MS1yBVe}+U3)Cw)QB z_PbaZE$h~fi`BVY@LYVfepg2U=XO0mMrrD7o^&%^dgcLGd4ItdXp#>VaV;q*P= zoVC~9eWF!h)>b7&10ig~(dDV+(007@UU>}PqdQH$QAFRf;xfjAa7xBQ#aE}}IY&~J zbB6r6RSzG$L8STGi>wY&PZ#7{-HQg4v!^PH(6{zrE+D5IiA1k0_&qrSxMywW>W8tb z`$OuFr}W?Cks?Dau<=ESRx|=gTXzQo`vT8lES>#+*D?1>`9nTViZ-u3yxZARii20@ z4}hnyU%Wf?pFonhr(ThUGuK%7Pd=DPn!<^A#GlTv5{gWg5nb6b4+R~!-d~-5PK<5; z7;DE(`YDWrdgvt^J;BTjL6Ip#N(>b)^@WJy$UprN6e-lq3`umfnE+eG*;Mv-0GxLc zUOoQJ%R&50W=v|>k`zf<)?MR|atKVXMt5B^)ds{KY|UzCmSl|tqZf%0k&(E_x?PZ~ zwyegwJB3qI4d_tTycp&32r6>?x^>(%7@+Bo_H6PixGeBpFn4YYZR zc{7Is^)eTo*T?0hU+2`kX7Q}laqqI*A|bIN?umy6lT{h}i4*XW7Y)*50nSOmxH)K- zIm{wSzuT`owR++)s=DWE%7!pZUndNmy}BA>FDfR%#F{mJ-pgs>4%*2adn;E3e5=V) z3AwsdbNzO+`1HcCVVj3~VX#y1Fkrct;%fPkbkR2GD+zO(xzkH;F1o(`y@TdOQPr2B z&LVrUQy$$@lEo|!N@CQ`!E~}ylRafh;Pli>W!iTdqnPGeQk;cR$*((yF0<{Np=`tV z*o*T#@$U9}*?B^=8V?@#Gx6;K~>Tjk-SG_;Xe5W@n z{L0(BX~wjkb1h1j>t8|kV1Kz~V)(y}j=l#@z3j<>gK*E#zBESl$abRL}!3GrQPGr4XJ_JpGG&aY3eD5H=(Sg8Ff7mH*gmDT3(;%P^LpA0Wu;mT6V?C zR|s@PDULS&1Fg(ZYo%+|Jg!4rkucSr<12exntX6zH*!|KsG;5}2`9z4H#5sF-_~fo zH;eji;7@g!7Q2{!=x{hX?CmIusN=Svb%9QmlW*Y~*4x1i_MEfV#RB%RGSJdW)i{kC z>}hbtg4)VjlnsLWSxT~R$hno(AmFye?ak@veYGPSM1&!F?iZxIA7yXC*6raC(OCG}_NS~pMhkQAHIy*1-kw40cc zA;G|Nq&^vqe4Recf_qZ|B571P$;$ zgl*^CfKkeEbs$;GLzFJ$T$3yGyOHC|ebt{}(4r%uZGv@aMf?eI1J7;Rq9oJ6TOX}G z*aHl)IZCvZrT=ij72RE*US9=hi5);oDBsK|RBiW8mT_lm{4{B_T8)w4(6-g$ zHuZiTer)RQ7K(26m*O=ZWo^!Ewk)rOkT&y@woq=vGd#;p9Z)81h|tP5^$Z2R%Nivz z)UdYHp6WZ*-GimMc+rDK6-3X;{TlZ38uZ)Lxf&RU8fXVO{;;-KxNZ1TpfY{e{Ti+F z8Y2WO9?0*{t3c7I0R}uiLxzBI_P~5MXu+Usf5a`*5yIziB=w_$wVV?#T zIThU8!SP>UVN&gBnLK*Za)=>u`O@8TWfpUqf|=(VYFd+)<0To1=uji&P~+d`Lt+4+ z=wWjYedtykS{}Vb}}@N9}O^V{^qMCC#0y?k3v|HL0Il zgy$o5>5dSGQT23w>CFKrGfc)RSEeI|^YWd+&iiM*_IoD6&hOu8BYqs4gyzsli=%1K zdfC_NQCAaZu5@2Mll8~nj)&~?xbdn9G)9Q6wu~;6ea{LupbNF4&?Tq~o|9B{3fLd* z4hkp1Pin+dMqj=_$Pp=nd_v>}3 zmI~z>1J~mN;r3JP4wj_&f7egi)*AGoB3{6HedpYlf zgJK|{ip}5pWW!L4L<5#$v`4He}E>wqCagI4247jgkSGOf36x1 z)ISdg$Na<=DZ*)r={iApEg$~hA(4Rp9rAMB0ua*e>aUPI&!Ni=7XJ#F4S_@=7~YQ{ z*Pp9ASwalrVohy>jlb0L(O{@wQ9V=B zjtQ$~CvB3yQJQp$Ju8p`+JOf~f|h`U!Jv@W3!ok6u(oN{uQSzfNRU_yTs9VInC|QB zppacnVID%Dt}+JG2k8HJK<8^Z5A3~`C?5g^1UJ)#KwO^7vp_dXY1)@L2{;@6yJ=Rm!GJoRh}Q5) zZaqc7_=UkJLZJ@smVZjmA7j3O-C;8B0kmL{jVW+Qc=<2BhVdV?2iARW zq$*p;4nx!PAsN#3*J4Mry_h zCXUJ3rVkU2hi%=il7;o{D|ff=^vuq*oX2~O_q$TIN45M!VA>a$y4`O5;I+KxdfJmG zi!bgT@1q{?0law%(ibxADY(hYrFEwA=2IFhR1N#I8%g)CX7q!XVQahLYm5TqI0mNL zn&z1XmeB=l)>dK0-b=nW?FhePX36(%p!TA=C(_5Dco``(f*eG;sU zGOTd+utcETf2LBVKbB%CxpDZ((&SRtK_I2h`%;SBGMOaw>v*ZG)+c&BE zH>dP|(d*xPsF~>J@7iGey3asRpVug|1t*`Hi82e8Eyy45VJd_wl7J53?8Ai6z z`8MH%K_53+k9UA;=~+D)S+}p7KVNI=JnaTsjr^3%qIy0O7kkX~1;l_2Jl;2^EMb1h z2Mx5F2Hn$gQdP(qOk}HmLqHE=Ip$Hlnm`9mZT`RLxj;CE)gSn$9tS^c8PZnx%S$rt zv!@5MyCTV*w5NmpBUtX2p+3GE~5Wut1esHA^^-5Q7CJ#4%0s_ql zwS+*!BF}W{)fU$x!YI^wW(hY(njy-3atXmS@&8ge481U|pDxT6WNChSzXmMnCOo*n zyNEP1jhsf5xdIAjic^TKXqap{s$sBEP+bSNKNF~riGQIi;bwW>~aIrbqv80nGW(3vjgE1m>GXEEh;KS0U7YXt^`^;rpvnlHv7`!Y@qY zIRSEvFa3h0#F@b0;t}S(>6Y%)Ujcmn#D`!b&NK>HD;AF~>m7S0e*>8&&Wt!fE5yeB zTa5GnRg9$u+i$CuD(Z|3eIaEE*5BINyce1Tjoi1GmD+jzd9*e;pgl9%I@sU2U;Ry3 zs9O;9YGu=C8MZe)ns$CHayNKu*7(YGJ-)wB9SCGyBQ}{jNjX?$Fnw;=w~XgT%a1KS zD4d5(8}MH}XABKoIITqHmBX74;$2EZ&WI&q8Z$8iiq5B z3kt%4ND*oiRe8SF?X}?(QhQ&mt$2NY?8MA`6>a$v50ZCG1MYX!lI*r8y_ITx|AUmkyQuQ-F>Smw)T{nAmD$A^Crh za4Xcx_8WP6RZLaZ|Mr&^sOf*Kcvk*5#B({o3e^9H6{AK+AtOf+r(umr_trjSo|98| z8#p6t*1bk>ZC`wW+-u_B>9VZnGpDV&rK6ke>^i`@`ewI(pczbWytD5%$a6@Zs3ltR zA`8WeWG>n09@&H^ z;v6`cXkCmKA~+N=uN)vw*HM6;+<1u+vmP&093WW)uKAAT)?|j0vfP}0Q4)ku5PH{H z&A#EoOTW_01czk9MYNo#IrMN3XEtjQ&W>awc`p~`f-lmjHI-;XHWjM*2em<*j=m1P zx$)BY4{CLQ=D$!!a8imd{z7dCO&D0rxi0*Y;nDQ{vRAR-%RstkCB< zL$bl531CN8?WA926Zj8TC5y3w>L$H7`EYD!r zs7f;#SEeEG4C91K8_KOr+DEa^Ac79#AV9T{tvW7qxKY_;2?IQ#qQM7{)lR zL07*Iy?S2_?%}Weg80%R;i5M(SjkYs@r~|r%V$hICD|2PtEf{nhWOi;#OOKgRbL5(y=@Kt-1K<)TU&LkO6XDH+8EP~b?RS{xD~yilgYiz*$GR;Y|z zM4Eb5N-8D5sWN+G%i-5~;6=+g6Y{J?<*Vw!p}Q!j^s2&5CR-~tP>P{L)4?c+QRPQ&Oi7j-@6Dbj zB~HuOKao(0pr=evD2kB>p{OcNc()WG@Y+k?iHJ zNE!&U9W~Vk%Ad7`oni}L(aHbfdkFpqdAlp}-UUu|L9&tyH>4>1JrvR=a6=HCoj;LB zt$J28tt=8P<4h2}DQ)4%AqyhNX@cZT13(!t7d$Ox<^iCbR4DO3D3gHvlQLKOoVl(3 zNTwl&qF78EnF2aDzeT~XNV7#AR?%2v6K)0x!t6{8%H}t_sd|l%Rq}EgzeDA2)n-p2 zl*tu7EDK2IYe{-*=V+xP@P#8n3kUeJizA_!FP!Tk2gRF8I)e5X_GC)x{TYQ(Mi}!r zl3QB_#kcJtm`G@xqc*i42h{#$-)^T7KkuA@HSlG|-PGZyq(N>_ukupRbQgetiEs%8 zWRz)W&sg3v&haLvU6HJbmXj%$Vsyj0Gx1OJ{T zWlB5Wt-GUb1X^fi{Q@UAcrL20jY^*FffAxLzHJciNVKmTy5l&8hk_6f0$uiF&uNed z?$?VyI7;jn&jBfNJbM-f#{JueZy_2aR{+Nub}6gtkCaklxWYe!?WfljKS(;M`&k3kEF) zP~!Gjq>Cfw6&*MN#gOEBUcMwsacrMx?+zZC`w889Y2%CLO}g)>s_8^UTP-*k5Ug@> zr~uKMCIz0NquLE(f|TYcs-2eLDEd7u-cd9&Ezxlp9!i=k8-i%xwJ z_=53|Qw$;w?Ir;Fk^#X>F1MmZOtI^^EJ=u(MEmP15>cvy`xJ-wCP=$ej0~ni*1HCGx*z=yHL~OdSs*a}Z_1hRp$y zWy`B-uqq8k46etOZM}m7%$hYjga0~BybnYj@92e)>xco^*L@C>F5dB~!a*tvHY_cA zN`~vrCyxkI{^9SiBo!iB321+X<;gg#_>n?K;~T&%nXHU%saNS-DfpWuL>W9YttyJf zRB~GIVnk>IzK(L9zwv{q;Sk}Md0;SKy z8K08l#o}?HRXCJpEL~G^Y%g)&pK;IdQTUj)APB)Kdxho0W6y}p2X86Unu)qL9CL!a z7tyk}YnbVJjz|@OD-~@YQ)ZOO(OI=bWgaGEEcDb$d13OXYr2tWI>I%vu7Xw6xUi-o zo!TjA@zAC>gidS1EG({4Zw|Xw0`cq*8V25%cpIEkOM;uK+1*}*b5x-gB$C+1z4)C@ zfPL|&403}vN?x&!>;l9qGCd$@tyaF7lh+i^Lv%5(mj>g4;S`2Jq3U;l@cJ_~cMvTBQidsV z9h54>9cpe=2=BgYJDeYAEu24{xo`OpF;?(xUwIh&4sw_f$uFNR@K+GRhG^tc$8UlxhO z1^n}fKMcV-G9iSRj4eXQC4r)TfgT{HH@VMf{oz&*Tox`s^kK*&j_e0iGEXQBJLEXZ zpgZi`1^B-f*^kM@w_=QtFW{F52bce!Z~po8e912ntgs}@LEZ8%qrx%2K!0H$AkrsQ z0d9jxNBVib@l{Hb4rT-ByR7DN{}qp85ABZ^vSU;Si1}#WiSdR9nD86{p8b~;(tuw> zP=7ln_*PJL2T7SZ@{;Uv8p89b2KKjOSVFbkh8%fK0u14ieW9&>9Dbr6vG5iZJg;2q#U63LcvFoCk>g><5VE z(~JPX_I2MEtCr2CAcXUct|6;qt|5X~{kL)b7pdr5ytyY7N zhB^3B^OSb-#91DLmJB%dYl_muM=#s*js)<2OCpwFQVIw>8~mn7<~f4jx6K~9i}Bj2 zw`Svyb@*a-mQ9E-z`$r&-<4sl;rE%Iot5$h1Np(r0@Q$q$_?uKnXIwIwfcTND(}>f z3h?*Ee|4kZqZ(3b9aefim|!y0s1B`y)4;B6q;ss^4#S^|`S$Fc=9r3oxNc>!@g#AV zoC5}S<2<)@%ux>%)*%B5owku1pz4~5FJHD1umrfIILbWvvEcbym#$31P~};(&ADd2 ztw($tyTfZU74u)(Vl4B#=;*z?3j&sR`I>VAfGPU}Q}#EEo_yJh@)Z7`@9AWpSRsKG zQM>GPZ0rXEpt0p$2;Get@52zf1{tdvFks4JgalCbug}(cjwIQ{oyQ#H$#9(y|NB?Isn?D z;Gk8j1rd7AN#aE|r#!2WnLO8wV`i3Hrl)jTKY$oqSqCHUohxAdm>Dfd_K7d|Ov2?H z(^fCZIjv3U5;53 zhTGF6eJ7RpaaDs1aQVB$<&b8rGI^?_8DrTkp9ixV7guJNMU^)F0q7Ony6R#H<=7|p z4iZjhZ4;RMK$;=#_4h}@r602dysI#H^2E#YjN;Gr{)1?Ifh9f=i<0bxYK_Oirgk1v zMI)42LM@Yaa2%XDm2q@39@GdmMj{pvyBfRubCB3Y8LU>a*Z45q%+2`yGs`g}nZj0Y z-=Fsdds9afOSqUc7IO-2 z?8j0~91QuQA_eo*>s+I9Mn-ZiLq$$9BSL$gp^h>tKP^uQ(Gu*SETM zITnR;FsGqBY+(Jm{sW%XhFFNZBb!5wn;Oo_sE3=-UWwZZ)J0MLh-hu{c@aX;m6mdW zDjnqSRB87hmu= z!q{!Ar3wc>K63u9dR)v@9P;hMr(Nv07=BuaJ0VDy7aa1B6~|>i3KMBa1#b_c$kOsDn;f}!fOh- zf9pz~hYDr~xe=QnK!J?Or^N~2{0iDv?NLX;gAlX#Hm}VVI2e!d{PM7m!5+xL1Q{cU z;9O(NXa^+PCeD;M2WiBdCBZ$^C`1Z%V2IUNg11V#e+|RBQ%hA*rB#}(n7kJ3tlU1& zbzH2dO)7!&wsp^tvOsEZ&9e4d4oDZp5drBUix(6d;&HLTq!iky@AS^vLrZ>sHWV%8 zR#f7bWsFHFjbv1dtGUrDDC{OEyyT=QE0vU_e{teq^=kI#rlNF?x1lVOfCXB}T^K?f z3eRDU8L78bls7MOL~8(NSS1zWW)|V5rmocnP+Hau`J0lcC?1lAyr?TS_OWR3H)z1Y zlZx_PS^j|PnS?`QqAvn)mS}SC>nC3ov?|JX7G{Hej1$&Fe`!J?`X>vFlq7&N$D;pn z#u){&0JGkupb;w-eo_i=+Jd#!dC-fDiqejnQlQ5DH%`ZqV??KDqFgpiM6fzZz?8LA zg^J0#Xi684E8Lx6X#%gyM%EYBZ)-mGxnH$BLh+7!r3y8CfUbMZ&lMk56Iz9s_MB)u z#VC~90(Z*E)I#QzUgUoPZjp54w5yWSy7{vE{)+n zU~cfgUMps3N2;LQF$cl9jRwGmc2pekOY6`;w)O2g%N_YRHzeH``FG$j}$JrMhqB3c5A$hmpr$a~O zM5(Zr0!<-#2_xM^QU4donZ%_|)h;b(=>!YRqh?3qaVx@y+`#O}4s4UJOeV2p4^tL@ zk*a(^^v@2=rojg1UJ;VdQMEkUQM4s0+oy{(PW#)C4Y)siQUC~l8B&@G3g9EPmKDtE z&KHw0zb@*m4QMCs9w}M`XBg*7Q-Dhn_Y+L_($YB%9B8=XR`-%iiGi-u64w6NAK$vX zs)FET`l?jXdYTa1@)P8E~?vi5Z& zIxC~5YL2ck27p5{dpR&b0uR`ITwiv+|C`apqzPNDy+)Rv2xtE-j0 z#b4{JTGY3lzjLAW?v@$0@0&xnI>{bVpOe+^$Whn=HXi_L{03zA$e9RNps-82899+^ zo_>9We*ZGt*XuGorofmD@175zV$2W27`Ol=N=-|{@vyK~X8UHu#*`W!$wY0)&*P8% zt@Gphke}U<-Z}%=3yI?R*r&Zky(B4ux~)H=BOV4U(58aB;a3GGs{Er%&1lI{q^7o_ z__YxVxDdv8-zg)WJ(;Y6QYm} zWwUP14}%`!_9uDwALtE-)0fvnd(qB6l{M-xPK!_Bl2ggCf-r|O6yhWK9p8eA*48nx zx@a4mLQah4vtOs-c_|Yxyz)aRULCW+8JD_Z;xp0UPDG~7Jz}C?DaZ-0mhgp6!LhQG zc(b{Hq7p8*P+*~O!K1mo5M&_0a;}$RDDX0l$pIS`*eJ997LDAJKHi=!%nzBOUnp+Q zcM+3*Y>gkZp6J#m|4elcpj%lVULB0b=d)f{S{GPcT*c9rfKS=3;D~>}6GUiim5|(t zh33HsHfeAu?@L5)2eFo$>x(N;K-qkjCyG$1u>x19o#^<&G+sZN?Hh!vw7oW}{J0L+ z&uDt94??7SG$$EV78D2OXN`cf@u zk0W=c6u$xQJLv$M{NNE;BG`nh6AUzCkd}`(Ohj$s`N};SOHAU`g((Cl=l=bWR92ov zPdj`f8Gndr)RA?jThBSqpi0|sf))YpOn48Q2BPlyWH@zuUiobCf;@+cTMyuN#yzZw z%fX;SCs-_btSLv`FG$VzkvJ{EZZ77ug`7RR@*;W32iEO=PiiCzf}a&;HQKIq5!UC2 zYJ*%ibQSf8$=)3mQCm9DTaFTn^p$hG+m96vbNnrmedch4l0)0#n8UwpZo(ml*KtKA ze10Kf?+UoPTJU_W4nbF{H1lbIJ{1w?)WPLpjWR&*t(uW&h~S~Kt}h?SL{#oHR_fw? zY#g78eJs`IxHfA@8|k@PoMQ0SIxve!vU7T4uf4L!7V&evZ!wjpE9t2m74l$ zW~}mG7kte8fb|d)9MHUX@8D6Q%AsjAvv48{7JgTrYlK4y>+6 z3gUC(iXA_k8HPQn0=q^QMivP5gZ!&yQ^Fr%+a{w_P`pqv7@1cv$=933`jz0X2*Tkp z!z9KPER9m9NV+536WG!Dc=+p@cni45#ut?K!2&tk@`X+$<@#3WwQsdl1dB0uZB$&b z(u1s%O~AOTo;N}g$n-Ol3vsPmh4g2Tr%E77-D77Cb5Le}8TTKArpb?zT%mjfO?J)7 zPTh2?<7dg(T0Qa@J!kA}R>6xLMs&W4mN8SCcOech{t|FG?fs~3PVn%MCBzsuAE%pk z_u&6@>^FD5iwU4^=x-Q6(WrkrW)3Kmt7>Fy`&aSM?bHGL?@V~3KfXm$rDaRoCbgg; z(~O2TPa)uX1EJ#8V{9f|3wA92p5?WTW4f-F>Uy+`_qwK?CIb$H!gdvr1SRJFoO?0; ze0;~(Zu?5tNFho?)Bd9$$B}Ljl)2dFh?nrSXDZZ$I_3pQA`o>q1NnHXl~H@ioT^6$6V=TjOo}0_v{*r8Aq}} zwA{v=^(9i7)`b`BJY1}GB=95)850GO*dM7l&Zni(@v zut>r2XfyxFr0a5H082*f(Tjt|lGvOrvxY3&QVGeVGU!e^%TOw&D6RF74G4^JN`vKfX4%v??J@; zNbMG+Z13s&6_lG-4}4v1g)S#w`_N@+q=pwXYo3Xx8#8XYn7Whp{R9`iXTgIbNcs4L zj(WG-alfuvwteo%`T73l(h%b1-9ySErADdJH4PJ)q} z$soMoJkXk14D=@$(w8>oF0MifD>b3Zl9=$t@f=FadOFJ-d#QN@%ke&#@5Uc4%%3go zeFoEgdHnXWa1n2F_Y^2n5!I8pOrE^)+gA6f0) ziFQj7p2x0Ryn{YQR&n+~UJ=NaC9Yej4{X-z^x9$SpfEk5B-`ie`dg1-kaek3+UdiH z`$R#HraF+#2+-~mVq_44AQM+fORPJ#=sb~rW84;meP7*PfZBwDLo0E+1_5&UAV?tW z-Ls~{Yk}Tu8OlkLq|HRs;POs+QTa7p)-8jz9#(8O@s97sBp5ab0_IFi`CXm+tx1|$ zLW(p2O_Z)scK&HdAcsK+Nm_H|eBA@Omlv@&wH;X`(i5!OWnZt9v>9+mUF=+A{3^=BXedMnZ{=2g}S0;$!vEg zlGt)a4AVkLLgm%8%jRupv~@^hoP6YF@>*DE0u~=Kp>~*o;l${AT>{eWm_&`SR5v*QW(& zJgI-z@Aaz9pikEyNox4AF7N&BVB^w(|MMY?Qmh>wB1~!v$U8iU_YCMtQGv#wnH;B) zyZ{vQfXsD>(B(`7oFVWjxN6`=mN-DnA7K&SF>k_HC%m26vz)r+vN1w(EH$+K*bB!v zN_nxMdL5`O8B1zY?(GhwW1S3oJ;1R~vD%&{*egl!1cza(_>x2}#g3#1CN?1sOwq*^ zv4G?aG@AX>)+kF!UIK)#KOfYoff&+*YWgf|Jt;O9W0FBcy6J57+k8Q>syOuhiI(kAv{exCG5c z;q4)lHN0qR^FbIFbfun9*$Jq^VfRX6bFZw6uj>NpeGjHxGMlz3@A9n)YrFjK{XE;D z%|=FzWxqFt1v{(Us9kQu7M3~ohj+zU0_MEGdn)k$u(BvGqpy~(`eEv|7J1|M(6znb zHmBpdiEAY6K9Zcjsq5gcn>l(uwNbQS$#)dRSQtRtXZ<}nS~CKV6$=4Ge1uZ7y^8^^ zSQeeOWhmXIakz*mUw0>u{u_-!`D5qVJ6dWg0w(+8PYE7EG@a#$)s@MUkb=U8Zz>0= z$=w|^%gd{jCF0ljRuxJSSPGJH(ue6@N58HO&XHq^2W4|!k1`ZeC8_0_aI7esP*9G_ zndHD<__F7mqcxnf36MMIg~qhf>U~SxTZ_t>D$?eEHW$`(Q+*!{bSU4BT;;@YJqQYF zZP_X3pfeoGw~IN#m+*Z8=^)goK~8MgZM7X(T(G_ZAN!eL|J%)_une@uRy)<^IlC5h z5h6NDZhSkOOE;yJ;SRpElH+2MysAabYDIflJLRWZ)Y$~mz9pM*E0egVF-?mbn+Z*q zD+$u3TMj4_8EPwNiQ9z|4^R3OsFvGDziB|l9E%#`?FmS#?+=G^mwjPL5X@N^dD^}m zQP+*=@M8tpP%T+Q^8!7^rD zu)4NCGrE`3Rg=|$8q=vkfrQ_01w?*VjB=+@9?aRw9I44(?J*l_av1Hi1hp@xF zJ+4qxDez4U+w)pbqj5sg5umqIIGD~;EcTLZQ^N%pyXhl10j1K-=Rfj>dXbS|Z;awq8{~Qq!lHx+6=cS6PW7AW~#`Sq-8$0e*?67@7Y*vjllUOa+ zjN=L#bA(+oMng)74ANLrDl#~N95G|Y&{l&Eg|y}cki1lllO=MK+h89WgtS|$tC8Ec zyb1l1!ve#4-p~CEI%wxOuJ<~6&3Jq;^yM&V5ezk%V_0l zX7|_6UQKiZ%pemQ&`)2niNuLgo^Z4!Wh6@E0+?E<*Y6OC-HFA0z zK0pZBc;wr}q+A?fysyRO34cDE2tFW%T!siiX4fKFs#r*9xwU(M6DxpX(o9UC9qKY; zEQ6^BgIG9Lz@OEGL%)wZVl@Y%R*nQl*=nSEQ?#(*NHCsQk@s!*spDfQBEK@!LU0*f z*U;x~lZVu`!(^E{2d`}W4_*9|$16PmJ(K`l1pm0;-HSk*2>5w}oYTAAX1 z=J@EuabdCUXP-jU>eNgt&U67>{CZRLWx%?_)bcJuREPw-gQ?7+W`Z7xPA#1%bt;%! zlffj{t?|a#Vm_<$WAUYrxQkK+Pn`6@$hH`Z0D7mvDYH8WzthHloROxv3&n&h>?ys0 zEpp%ldtGu4=|f#kO}3&-UEqD~9ps-4+R`N}w+|3(3=od>58;0#D>^$ky8P9uIq?nl zK}=|3Uwa4d4Ms`saEk8gl2d5v9pnm^VIV2TY+Ell12+Z`m?nYtIY%`!Fn>b-nRO@xi_>M!9{=fjNDs_HS<8;b2 zQxd*(S)6CtxH*$aS7)<3(9_COP->y?b3!I+mVdqW3E8PVim2Hc3I9asj=Us$H2lT4 z!|cu0ST+gy>milo*Z&x$#m|RD*Z}5aA^_Rlf5sg4Zg$3I&VX5&KiS=k=C=I-7go=1 znP7srZc9WG8yH)2p`f~6j5uk_0}usP{YBn5Gt<_M%(cl3ZFP=7c_gbWiu+Uys^2d6 zhmZUl_^-Fypu`=72s9Utn;@OCT@ktynd#NTkDqC06jwi$p*K)aX6M`e1g$>zKjP+o zBX2H(qtMiqQEBpgr@UMLE}0Q+%NxTPKKPh=pn02IcVstRF{43?N9sK{vgnS#XQ~mg z$1JTf!Wv==sZy0+^v1#gb9le#maVvLqVCjP7fu;ou@?yoQ__iU6M|H?c_i*cp?CmJ zp-I4|p}250x~g8UG~lGNQou~A)^8~xl`(zk{2&}vAQ~cv6?Ig&PgtH9jd_5rQp9i`xdq8-AdFq1daWL^VLQ75k3dQLIk832?d)`a9t#| z1m*6sJjqM_7Rq+X690y`&XOL^uL@HQ$EMVDBQoflLM*L?SFGvUt+Dg;3BO>!6W=>a zDrOX6Qcz{hE|JMeVrP5xXDEE*jEm1fFl0}Ts-U1T&49b7TM;JJmSa$grY1ulfPGaO z;Sbof{-^`2z_lcQI6ul7hf)4bH_9SWV^nb&OgkcU#>KPCZ*E*lJ})Y!xotiTz!@ulNW<1UJZ)0mN1f&g5&9w`Z9 z%DH>N%9Or6)wJ4PDUY?sL1>$v`Td)oTUmY4n4VkVUC04F4ZYE$5N|O?2>Tp#p6@-V zy#oyC+=`4l94WI8ZPCpdpO)$Rf&hXr~=yCpr>V=>%=sBrz zf;drPyF3%j^!c)j-F4TZX7L79iy&JY8l#uPoz(wr`u94zfrp0AF8(*3)5=))&t6zL zaX4JueFR8gMSb0$9(s4KE%*BZJbHAjNjV?(UWc|Fb@lls7KXLh7|Z>9tXH)V&o^Vw z$R}(4H`Db(@N`T=-8k$0a4t561=tYQh9a!f+#ws3p9MRgu>b6_3ddJ1dcc8zf{B2D zFabRlpg{Lu-4tNt1-L%#jjU`LtR2lPuGMWEQPt4+ySzE6?Pidu=+PhtyIA^*>4j-Q zIIO;xCUX?i*ExksCCQ;?QW$+*N|nz^cm;Y2EnMZ+2; zvA$#HLo)!q+uAOMRt!!sn7!KGc<;@A*mjxY_CB>Q94j;!n!iW!hGv2THS-X9Z0?=1 zZ>e?Us*=7!=Wqo&YwH-?2eJXNgqtjES~k}1wC#}Eg{6xQWE&4~mguHv`XBc)u1U})0 zm}l4*EQ4Y?j6B-3%u2gEt{Q%u5}Ytpjm}B@@uq}2RFPR4c8gjSsAQg%Ut2r%SK-I~ zPmrJ~=+?5vNMt4ABMiT-Czm@+Q==J`a%57y-^riTd+G5MpGi){TO*Mz>||Z$$-hgl za2*9ARmY4RNqFgwG`F$F&jpP{!-7Ex;qz}J1y31*P$mb0GsBU1b?=1%X@fn&Sl+RI z6$JebvBr%SGl?4MW#%rB6H^6OtE9n&yA&*m!JOpNeHstLl7<#9%#`qQ#Lp6kR@4$d zjh%ieshwBr=1w$A6K<$lsRNh#w2_cK>~(zr)AzeS>9jTzzmvg&I9I_LQg9GqIAtm6 za(C_dV_YsccpbwN%-0qyz9>URb;5QY@Iq?Yhi8Fp)9+se>d+W-^;m|_3z^> zJ^ov&dBWS9e>dDd`Sy_=yYTkeubdFjE&aZ~v3&g%>Gjj9*j{gCU~E2W zaLeFeK#oNAjFc4~M~qV>i+EGpG+#*Cez-U3y7#)H{HH%izJK)~d*!aEFTWN>u5<{p zl4-em{muUyIYw4l`Zv@^)Y-F3UcHxJti}CL#`&OoX&IuN*Tg5Wj_QgEI zvro8Ke7d}iI-68Fe4EnQF1%k*Cd=v%?`ZeU=%&x#vZ)3vYWW*l=C7Va-g= zXuf%!%5f|&WTkG4}*||J{}Bjl0+M z(VyuJpUZEDZCiirW#AUS6f7UqU~u!m<(n1flim0~2&u927o3W0*jlGxSe=^lcGHb~ z(VKi`>k{U@c<^XW=#q=V2}}RW1N`Hn4_t6Q_EM=(P9u;k1aauVG;Kdn;u zmzWyS_ra?6)PyT0B5x$PbPgBlJgk_Pka>pr1$)wy_TJOFmw4l!T_Jl*zMDPH*{H06cKHWCQ=Q zCN`;z1&vsV|Hd8P4%r8IGct)VgNDaB7$Pb-qSpp3oj4WPLOlf>a|3ldV4wjg#=sEm z3>*wj%`1rysVqn>js>;O&<&_m^LF?FRC*6Mj?4|y4Wt_wj{%Kz?HNBJfBwy7n)(Z;QVMYR_R|U=TpjUJn$3YfmgFz~<`U#1HfK z1J!l%p<6x!7!cr@1E3M5xj?^T)1KELEo20={4~%A85GOgfMEdE4)Hm%Ay`9!{mS~8 z#rnXea40JSKZ;48c%UW~msIAYf~PCd!vOt2Ntpfy#z{6Pn$eDzL^lV0o)lpYa}mrO zq)8NX?dTIP2<;zBq1w@=VbD!LpNK%1pj-|$0cBbOT|fG$F+%^gN~nJHF=TWj(EHyA zBi1)y?Tw=wfZowX7|`B`Yyh@yFS=RieMf{@4Nb^qq4p%vO+oK!AxxRx3=I{Oju*Oq i^!6Y^zfn6<7DH_v26(dqa}X$32{I%CvxiDAhz9_j*KU{q diff --git a/test_files/knowledge_e15fef20.txt b/test_files/knowledge_e15fef20.txt deleted file mode 100644 index fbb53a2..0000000 --- a/test_files/knowledge_e15fef20.txt +++ /dev/null @@ -1,6 +0,0 @@ - -Python是一种高级编程语言。 -它具有简洁的语法和强大的功能。 -Python广泛应用于Web开发、数据科学、人工智能等领域。 -机器学习库如scikit-learn、TensorFlow和PyTorch都支持Python。 -Flask和Django是流行的Python Web框架。 diff --git a/test_files/machine_learning_990eef59.md b/test_files/machine_learning_990eef59.md deleted file mode 100644 index 003b59c..0000000 --- a/test_files/machine_learning_990eef59.md +++ /dev/null @@ -1,44 +0,0 @@ -# 机器学习入门 - -## 什么是机器学习? - -机器学习是人工智能的一个分支,它使计算机能够在没有明确编程的情况下学习和改进。 - -## 主要类型 - -### 监督学习 -- **分类**: 预测类别标签 -- **回归**: 预测连续数值 - -### 无监督学习 -- **聚类**: 发现数据中的群组 -- **降维**: 减少特征数量 - -### 强化学习 -- 通过与环境交互学习最优策略 - -## 常用算法 - -1. **线性回归**: 预测连续值 -2. **逻辑回归**: 二分类问题 -3. **决策树**: 易于理解和解释 -4. **随机森林**: 集成学习方法 -5. **支持向量机**: 处理高维数据 -6. **神经网络**: 深度学习基础 - -## Python机器学习库 - -- **Scikit-learn**: 经典机器学习算法 -- **TensorFlow**: 深度学习框架 -- **PyTorch**: 动态深度学习框架 -- **XGBoost**: 梯度提升算法 - -## 学习路径 - -1. 掌握Python基础 -2. 学习数据处理(Pandas, NumPy) -3. 理解统计学基础 -4. 实践经典算法 -5. 深入深度学习 - -机器学习正在改变世界,值得每个人学习! diff --git a/test_files/python_basics_db5ddb86.txt b/test_files/python_basics_db5ddb86.txt deleted file mode 100644 index 8b3388a..0000000 --- a/test_files/python_basics_db5ddb86.txt +++ /dev/null @@ -1,5 +0,0 @@ - -Python是一种高级编程语言,由Guido van Rossum于1991年创建。 -Python具有简洁易读的语法,适合初学者学习编程。 -Python是解释型语言,支持面向对象、函数式等多种编程范式。 -Python的设计哲学强调代码的可读性和简洁性。 diff --git a/test_files/python_guide_a94a6076.md b/test_files/python_guide_a94a6076.md deleted file mode 100644 index 08a3669..0000000 --- a/test_files/python_guide_a94a6076.md +++ /dev/null @@ -1,70 +0,0 @@ -# Python编程指南 - -## 基础语法 - -Python是一种高级编程语言,以其简洁明了的语法而闻名。 - -### 变量和数据类型 - -```python -# 字符串 -name = "Python" -# 整数 -age = 30 -# 浮点数 -pi = 3.14159 -# 布尔值 -is_programming = True -``` - -### 控制结构 - -#### 条件语句 -```python -if age >= 18: - print("成年人") -else: - print("未成年人") -``` - -#### 循环 -```python -for i in range(5): - print(f"数字: {i}") - -while count > 0: - print(count) - count -= 1 -``` - -## 函数定义 - -```python -def greet(name): - return f"Hello, {name}!" - -def calculate_area(radius): - return 3.14159 * radius ** 2 -``` - -## 面向对象编程 - -```python -class Person: - def __init__(self, name, age): - self.name = name - self.age = age - - def introduce(self): - return f"我是{self.name},今年{self.age}岁" -``` - -## 常用库 - -- **NumPy**: 科学计算 -- **Pandas**: 数据分析 -- **Matplotlib**: 数据可视化 -- **Requests**: HTTP请求 -- **Flask/Django**: Web开发 - -Python是学习编程的绝佳选择,适合初学者入门。 diff --git a/test_files/web_frameworks_b8bf7b11.txt b/test_files/web_frameworks_b8bf7b11.txt deleted file mode 100644 index 151a89a..0000000 --- a/test_files/web_frameworks_b8bf7b11.txt +++ /dev/null @@ -1,5 +0,0 @@ - -Flask是一个轻量级的Python Web框架,易于学习和使用。 -Django是一个功能丰富的Python Web框架,适合大型项目开发。 -FastAPI是现代的Python Web框架,专为构建API而设计。 -Tornado是一个可扩展的非阻塞Web服务器和Web应用框架。 diff --git a/test_image_ocr.py b/test_image_ocr.py new file mode 100644 index 0000000..b5adc95 --- /dev/null +++ b/test_image_ocr.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +测试优化后的图片OCR功能 +""" + +import sys +import os +import asyncio +from pathlib import Path + +# 添加源码路径 +sys.path.append(os.path.join(os.path.dirname(__file__), "src")) + +from base_rag.image_processor import ImageProcessor + + +async def test_image_ocr(): + """测试不同模式下的图片OCR功能""" + + # 测试配置 + configs = [ + {"type": "local", "engine": "easyocr"}, + {"type": "basic"}, + # {"type": "api", "api_url": "http://localhost:8000/image2text"} # 需要实际API + ] + + print("🧪 开始测试图片OCR功能") + print("=" * 50) + + # 寻找测试图片 + test_images = [] + + # 检查常见的图片位置 + possible_paths = [ + "./test_files/", + "./demo_documents/", + "./examples/", + "./" + ] + + image_extensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp'] + + for path in possible_paths: + if os.path.exists(path): + for file in os.listdir(path): + if any(file.lower().endswith(ext) for ext in image_extensions): + test_images.append(os.path.join(path, file)) + + if not test_images: + print("⚠️ 未找到测试图片,创建示例图片...") + # 创建一个简单的测试图片 + try: + from PIL import Image, ImageDraw, ImageFont + + # 创建包含文字的测试图片 + img = Image.new('RGB', (400, 200), color='white') + draw = ImageDraw.Draw(img) + + # 尝试使用默认字体 + try: + font = ImageFont.truetype("/System/Library/Fonts/Arial.ttf", 24) + except: + font = ImageFont.load_default() + + # 添加测试文字 + test_text = "Hello World!\nPython OCR Test\n测试中文识别" + draw.text((50, 50), test_text, fill='black', font=font) + + test_image_path = "./test_ocr_image.png" + img.save(test_image_path) + test_images = [test_image_path] + print(f"✅ 创建测试图片: {test_image_path}") + + except Exception as e: + print(f"❌ 创建测试图片失败: {e}") + return + + print(f"📸 找到 {len(test_images)} 个测试图片") + + # 测试每种配置 + for i, config in enumerate(configs, 1): + print(f"\n🔧 测试配置 {i}: {config}") + print("-" * 30) + + try: + processor = ImageProcessor(config) + + # 处理每个测试图片 + for img_path in test_images[:2]: # 限制测试图片数量 + print(f"\n📷 处理图片: {os.path.basename(img_path)}") + + if os.path.exists(img_path): + result = processor.extract_image_description(img_path) + print(f"结果:\n{result}") + else: + print(f"❌ 图片不存在: {img_path}") + + except Exception as e: + print(f"❌ 配置 {config} 测试失败: {e}") + + print("\n" + "=" * 50) + print("🏁 测试完成") + + +if __name__ == "__main__": + asyncio.run(test_image_ocr())