-
English | 中文
-
- 所有的代码和文档完全由 OpenAI 的 GPT-4 模型生成 -
- -## 介绍 - -OpenAI 翻译器是一个使用 AI 技术将英文 PDF 书籍翻译成中文的工具。这个工具使用了大型语言模型 (LLMs),如 ChatGLM 和 OpenAI 的 GPT-3 以及 GPT-3.5 Turbo 来进行翻译。它是用 Python 构建的,并且具有灵活、模块化和面向对象的设计。 - -## 为什么做这个项目 - -在现今的环境中,缺乏非商业而且有效的 PDF 翻译工具。很多用户有包含敏感数据的 PDF 文件,他们更倾向于不将其上传到公共商业服务网站,以保护隐私。这个项目就是为了解决这个问题,为需要翻译他们的 PDF 文件同时又要保护数据隐私的用户提供解决方案。 - -## 示例结果 - -OpenAI 翻译器目前还处于早期开发阶段,我正在积极地添加更多功能和改进其性能。我们非常欢迎任何反馈或贡献! - - - -- "老人与海" -
- -## 特性 - -- [X] 使用大型语言模型 (LLMs) 将英文 PDF 书籍翻译成中文。 -- [X] 支持 ChatGLM 和 OpenAI 模型。 -- [X] 通过 YAML 文件或命令行参数灵活配置。 -- [X] 对健壮的翻译操作进行超时和错误处理。 -- [X] 模块化和面向对象的设计,易于定制和扩展。 -- [ ] 实现图形用户界面 (GUI) 以便更易于使用。 -- [ ] 添加对多个 PDF 文件的批处理支持。 -- [ ] 创建一个网络服务或 API,以便在网络应用中使用。 -- [ ] 添加对其他语言和翻译方向的支持。 -- [ ] 添加对保留源 PDF 的原始布局和格式的支持。 -- [ ] 通过使用自定义训练的翻译模型来提高翻译质量。 - - -## 开始使用 - -### 环境准备 - -1.克隆仓库 `git clone git@github.com:DjangoPeng/openai-translator.git`。 - -2.OpenAI-翻译器 需要 Python 3.6 或更高版本。使用 `pip install -r requirements.txt` 安装依赖项。 - -3.设置您的 OpenAI API 密钥(`$OPENAI_API_KEY`)或 ChatGLM 模型 URL(`$GLM_MODEL_URL`)。您可以将其添加到环境变量中,或者在 config.yaml 文件中指定。 - -### 使用示例 - -您可以通过指定配置文件或提供命令行参数来使用 OpenAI-翻译器。 - -#### 使用配置文件 - -根据您的设置调整 `config.yaml` 文件: - -```yaml -OpenAIModel: - model: "gpt-3.5-turbo" - api_key: "your_openai_api_key" - -GLMModel: - model_url: "your_chatglm_model_url" - timeout: 300 - -common: - book: "test/test.pdf" - file_format: "markdown" -``` - -然后命令行直接运行: - -```bash -python ai_translator/main.py -``` - - - -#### 使用命令行参数 - -您也可以直接在命令行上指定设置。这是使用 OpenAI 模型的例子: - -```bash -# 将您的 api_key 设置为环境变量 -export OPENAI_API_KEY="sk-xxx" -python ai_translator/main.py --model_type OpenAIModel --openai_api_key $OPENAI_API_KEY --file_format markdown --book tests/test.pdf --openai_model gpt-3.5-turbo -``` - -这是使用 GLM 模型的例子: - -```bash -# 将您的 GLM 模型 URL 设置为环境变量 -export GLM_MODEL_URL="http://xxx:xx" -python ai_translator/main.py --model_type GLMModel --glm_model_url $GLM_MODEL_URL --book tests/test.pdf -``` - -## 许可证 - -该项目采用 GPL-3.0 许可证。有关详细信息,请查看 [LICENSE](LICENSE) 文件。 - - - - +# OpenAI-Translator + +
+
English | 中文
+
+ 所有的代码和文档完全由 OpenAI 的 GPT-4 模型生成 +
+ +## 介绍 + +OpenAI 翻译器是一个使用 AI 技术将英文 PDF 书籍翻译成中文的工具。这个工具使用了大型语言模型 (LLMs),如 ChatGLM 和 OpenAI 的 GPT-3 以及 GPT-3.5 Turbo 来进行翻译。它是用 Python 构建的,并且具有灵活、模块化和面向对象的设计。 + +## 为什么做这个项目 + +在现今的环境中,缺乏非商业而且有效的 PDF 翻译工具。很多用户有包含敏感数据的 PDF 文件,他们更倾向于不将其上传到公共商业服务网站,以保护隐私。这个项目就是为了解决这个问题,为需要翻译他们的 PDF 文件同时又要保护数据隐私的用户提供解决方案。 + +## 示例结果 + +OpenAI 翻译器目前还处于早期开发阶段,我正在积极地添加更多功能和改进其性能。我们非常欢迎任何反馈或贡献! + + + ++ "老人与海" +
+ +## 特性 + +- [X] 使用大型语言模型 (LLMs) 将英文 PDF 书籍翻译成中文。 +- [X] 支持 ChatGLM 和 OpenAI 模型。 +- [X] 通过 YAML 文件或命令行参数灵活配置。 +- [X] 对健壮的翻译操作进行超时和错误处理。 +- [X] 模块化和面向对象的设计,易于定制和扩展。 +- [x] 添加对其他语言和翻译方向的支持。 +- [ ] 实现图形用户界面 (GUI) 以便更易于使用。 +- [ ] 创建一个网络服务或 API,以便在网络应用中使用。 +- [ ] 添加对多个 PDF 文件的批处理支持。 +- [ ] 添加对保留源 PDF 的原始布局和格式的支持。 +- [ ] 通过使用自定义训练的翻译模型来提高翻译质量。 + + +## 开始使用 + +### 环境准备 + +1.克隆仓库 `git clone git@github.com:DjangoPeng/openai-translator.git`。 + +2.OpenAI-翻译器 需要 Python 3.10 或更高版本。使用 `pip install -r requirements.txt` 安装依赖项。 + +3.设置您的 OpenAI API 密钥(`$OPENAI_API_KEY`)。您可以将其添加到环境变量中,或者在 config.yaml 文件中指定。 + +### 使用示例 + +您可以通过指定配置文件或提供命令行参数来使用 OpenAI-Translator 工具。 + +#### 使用配置文件 + +根据您的设置调整 `config.yaml` 文件: + +```yaml +model_name: "gpt-3.5-turbo" +input_file: "tests/test.pdf" +output_file_format: "markdown" +source_language: "English" +target_language: "Chinese" +``` + +然后命令行直接运行: + +```bash +python ai_translator/main.py +``` + + + +#### 使用命令行参数 + +您也可以直接在命令行上指定设置。这是使用 OpenAI 模型的例子: + +```bash +# 将您的 api_key 设置为环境变量 +export OPENAI_API_KEY="sk-xxx" +python ai_translator/main.py --model_name "gpt-3.5-turbo" --input_file "your_input.pdf" --output_file_format "markdown" --source_language "English" --target_language "Chinese" +``` + +## 许可证 + +该项目采用 GPL-3.0 许可证。有关详细信息,请查看 [LICENSE](LICENSE) 文件。 + + + + diff --git a/openai-translator/README.md b/openai-translator/README.md index 11a42c20..01623dbd 100644 --- a/openai-translator/README.md +++ b/openai-translator/README.md @@ -1,104 +1,90 @@ -# OpenAI-Translator - -
-
English | 中文
-
- All the code and documentation are entirely generated by OpenAI's GPT-4 Model -
- - -## Introduction - -OpenAI Translator is an AI-powered translation tool designed to translate English PDF books to Chinese. The tool leverages large language models (LLMs) like ChatGLM and OpenAI's GPT-3 and GPT-3.5 Turbo for translation. It's built in Python and has a flexible, modular, and object-oriented design. - -## Why this project - -In the current landscape, there's a lack of non-commercial yet efficient PDF translation tools. Many users have PDF documents with sensitive data that they prefer not to upload to public commercial service websites due to privacy concerns. This project was developed to address this gap, providing a solution for users who need to translate their PDFs while maintaining data privacy. - -### Sample Results - -The OpenAI Translator is still in its early stages of development, and I'm actively working on adding more features and improving its performance. We appreciate any feedback or contributions! - - - -- "The Old Man and the Sea" -
- -## Features - -- [X] Translation of English PDF books to Chinese using LLMs. -- [X] Support for both [ChatGLM](https://github.com/THUDM/ChatGLM-6B) and [OpenAI](https://platform.openai.com/docs/models) models. -- [X] Flexible configuration through a YAML file or command-line arguments. -- [X] Timeouts and error handling for robust translation operations. -- [X] Modular and object-oriented design for easy customization and extension. -- [ ] Implement a graphical user interface (GUI) for easier use. -- [ ] Add support for batch processing of multiple PDF files. -- [ ] Create a web service or API to enable usage in web applications. -- [ ] Add support for other languages and translation directions. -- [ ] Add support for preserving the original layout and formatting of the source PDF. -- [ ] Improve translation quality by using custom-trained translation models. - - -## Getting Started - -### Environment Setup - -1.Clone the repository `git clone git@github.com:DjangoPeng/openai-translator.git`. - -2.The `OpenAI-Translator` requires Python 3.6 or later. Install the dependencies with `pip install -r requirements.txt`. - -3.Set up your OpenAI API key(`$OPENAI_API_KEY`) or ChatGLM Model URL(`$GLM_MODEL_URL`). You can either add it to your environment variables or specify it in the config.yaml file. - -### Usage - -You can use OpenAI-Translator either by specifying a configuration file or by providing command-line arguments. - -#### Using a configuration file: - -Adapt `config.yaml` file with your settings: - -```yaml -OpenAIModel: - model: "gpt-3.5-turbo" - api_key: "your_openai_api_key" - -GLMModel: - model_url: "your_chatglm_model_url" - timeout: 300 - -common: - book: "test/test.pdf" - file_format: "markdown" -``` - -Then run the tool: - -```bash -python ai_translator/main.py -``` - - - -#### Using command-line arguments: - -You can also specify the settings directly on the command line. Here's an example of how to use the OpenAI model: - -```bash -# Set your api_key as an env variable -export OPENAI_API_KEY="sk-xxx" -python ai_translator/main.py --model_type OpenAIModel --openai_api_key $OPENAI_API_KEY --file_format markdown --book tests/test.pdf --openai_model gpt-3.5-turbo -``` - -And an example of how to use the GLM model: - -```bash -# Set your GLM Model URL as an env variable -export GLM_MODEL_URL="http://xxx:xx" -python ai_translator/main.py --model_type GLMModel --glm_model_url $GLM_MODEL_URL --book tests/test.pdf -``` - -## License - +# OpenAI-Translator + +
+
English | 中文
+
+ All the code and documentation are entirely generated by OpenAI's GPT-4 Model +
+ + +## Introduction + +OpenAI Translator is an AI-powered translation tool designed to translate English PDF books to Chinese. The tool leverages large language models (LLMs) like ChatGLM and OpenAI's GPT-3 and GPT-3.5 Turbo for translation. It's built in Python and has a flexible, modular, and object-oriented design. + +## Why this project + +In the current landscape, there's a lack of non-commercial yet efficient PDF translation tools. Many users have PDF documents with sensitive data that they prefer not to upload to public commercial service websites due to privacy concerns. This project was developed to address this gap, providing a solution for users who need to translate their PDFs while maintaining data privacy. + +### Sample Results + +The OpenAI Translator is still in its early stages of development, and I'm actively working on adding more features and improving its performance. We appreciate any feedback or contributions! + + + ++ "The Old Man and the Sea" +
+ +## Features + +- [X] Translation of English PDF books to Chinese using LLMs. +- [X] Support for both [ChatGLM](https://github.com/THUDM/ChatGLM-6B) and [OpenAI](https://platform.openai.com/docs/models) models. +- [X] Flexible configuration through a YAML file or command-line arguments. +- [X] Timeouts and error handling for robust translation operations. +- [X] Modular and object-oriented design for easy customization and extension. +- [x] Add support for other languages and translation directions. +- [ ] Implement a graphical user interface (GUI) for easier use. +- [ ] Create a web service or API to enable usage in web applications. +- [ ] Add support for batch processing of multiple PDF files. +- [ ] Add support for preserving the original layout and formatting of the source PDF. +- [ ] Improve translation quality by using custom-trained translation models. + + +## Getting Started + +### Environment Setup + +1.Clone the repository `git clone git@github.com:DjangoPeng/openai-translator.git`. + +2.The `OpenAI-Translator` requires Python 3.10 or later. Install the dependencies with `pip install -r requirements.txt`. + +3.Set up your OpenAI API key(`$OPENAI_API_KEY`). You can either add it to your environment variables or specify it in the config.yaml file. + +### Usage + +You can use OpenAI-Translator either by specifying a configuration file or by providing command-line arguments. + +#### Using a configuration file: + +Adapt `config.yaml` file with your settings: + +```yaml +model_name: "gpt-3.5-turbo" +input_file: "tests/test.pdf" +output_file_format: "markdown" +source_language: "English" +target_language: "Chinese" +``` + +Then run the tool: + +```bash +python ai_translator/main.py +``` + + + +#### Using command-line arguments: + +You can also specify the settings directly on the command line. Here's an example of how to use the OpenAI model: + +```bash +# Set your api_key as an env variable +export OPENAI_API_KEY="sk-xxx" +python ai_translator/main.py --model_name "gpt-3.5-turbo" --input_file "your_input.pdf" --output_file_format "markdown" --source_language "English" --target_language "Chinese" +``` + +## License + This project is licensed under the GPL-3.0 License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/openai-translator/ai_translator/book/__init__.py b/openai-translator/ai_translator/book/__init__.py index 5b688799..510e31f1 100644 --- a/openai-translator/ai_translator/book/__init__.py +++ b/openai-translator/ai_translator/book/__init__.py @@ -1,3 +1,3 @@ -from .book import Book -from .page import Page +from .book import Book +from .page import Page from .content import ContentType, Content, TableContent \ No newline at end of file diff --git a/openai-translator/ai_translator/book/__pycache__/__init__.cpython-311.pyc b/openai-translator/ai_translator/book/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e166467267ceb02ef574072fe2faea441823402 GIT binary patch literal 409 zcmZ3^%ge<81l7yFrltew#~=<2FhLogjev~l3@HpLj5!Rsj8Tk?AU0DDQ!aB9Gnmbs z!;;Gy#hS|g@w+#SRo>31-k_eaQ&asL6PX#VJ2O8$<^rrl)E$-{N-8&nrpID+#GA zNWH}l<=o;4NleN~h4TC~Ic~8e0kst|1I@U_QUKIh!~$ZnCqpzBu>yHTY#@RiNUUV| z3?v!8Mgj>}tC(l=8lSFP2KczG$)vkySXb;GH#lAq|12ZEd W;|&I}3#bU2i1`Hu0;q@wXd(cc$aRMR literal 0 HcmV?d00001 diff --git a/openai-translator/ai_translator/book/__pycache__/book.cpython-311.pyc b/openai-translator/ai_translator/book/__pycache__/book.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8cb27322d2700460a8a8711dceb3a59edffcf3fc GIT binary patch literal 919 zcmZ`%&ubGw6n?Wi8`qc!9$KxS4dP|R_75o1gCNCAD5PFOU>Gx-bnRxdo!uPN94h3H zL+|NH#6zSM{9CFN40{y?Z;{xGC*Rx6MvMA(_j~hx%zN{`*>~k~5l~)^&)o#|XEJi( z^Tq5wE=Hh0F&{dt&KMZ514aIBVSl<}h4omp9Y`e`oN;*%SRnQoL z3w5Rdx3K}#O=Y;eWhisN>sD4M;Samry=J~EuVCV0b| e8F_kw90&)-OJ{zO| zWD#R{qczZ%Fyt8eGODB?LmXD2Nq>dDiZG`|U!k}yl|lmB?tFsWO!>Qw890GmIxK#& zS3cS+M|VHit7rD=Y5wXmrc|8*F%ig)D@0NhqSIA9ycdy_gxK#%Ka1!R=4u*HUqd*C zbaQ$PZIPFN!qFze!Qz=3hIL~K3$U40Iog>5s{cY(nhI7OGo3nXoc5uM`Q7`c{UAH< kre4MXkr_Qkn3{~S2`rype5bs?u CoxHN4B{5|YrgG=<@I)PuRk_mVg`b~yKXP%oLX zs?&^=A5_{#*3ykmEe&cnF{zdKu}MhX-$~cet+1|ygfua&zm_Vkg!r?4-?{d+ZywdA z9bbR dRX9Lg0YO)*u_@-fbIjZCJ-6sQPCUp@ja z?+J}&a$_Ser>nHttG3ee3(&km6n3@*&lA`q^n7gNCd4)RTx;~X*{*mqa5$gSl$;iS zV@er~c}#CInM+M7$)xE|CMWZ9A&d23GI_p`%332-gyl>zK6El}y5ju q(F|63kkmj@rZ-PB zW0_nkYX)k(%H(sV8@Q_#R6rM1C3}{(!6LLBK?Hz`JCvZ%Ca9pOGr5eGOcpm(&(K}# zc?7Yy0GK6}=u;)p@NTR`q>?xns%+YZ%}Awf6Ep)Q_%Vj2?LEUR6b(E5J5YhslWXu^ zU8cF~z=e-(PN+|64OEa56E tT0;a z8p-`QmCeXf0~;k|akn&daY{*RiYyiVJEb!PO&ZH M1vAMw6xowYor~ zPJ!C2hn0|tOrQ}E77BC%I;^5K;MK)-j`h-Zr^qx^jB0UuP|Bk}T9>XZwsq(RYg7sO zpXk~ORiztUvEL1Kx)bPlSXpZYnP=)0ACCDgqhoFg!W1$688wpwyUe8(Q&2TZ(YQ=8 zn}K>5tKMTHM_Cl0c+41I*Af-EST$GrIMj W2!xG5^uvH5^)3c_ z^+4}uiQ9p{oha` !fk)N9BD i&kfd-Bh5WJy-X0m!l7zQJE^-MUsA9kj##3jqN|kM+4)-j= z8cZGsfz`j_t`Mz3S`&7CrHh>rG}{gdgfyqSb&Ltlv2*Nr>JA4ECv)f(t>VOY;0Y5o z<`5^AMU{p%+cF6R2`A2*oQ7IZEzVr&n!DED_}0)ot7XFWSJQ&i>M>_z7EjSci>Kq` ziUeuh8HGyuvl7i;P^DB(meTobVKS#mnVh5nhH??23=1h53P+MtT8iNfLS>jj-X-PF zOhC|) OVgS2!E?l1%%n^a zS7L$3 qHagc5}iB+as%fK4y9HmZ)D6JTDXz5(FDEV&yB&z+jzQVw+* zq0SQjjhh6+CGFDwx&7}6^8>%}T=gslI`u&3hke(yh5ipNynpb6gP--?q@Nc5^xUV< zEp`p)T>~sgO_!TyjHVeqICI|{y5yPj-0?;hy^;4e85_4R43xb+hPOwrJ(g#&wE?z* z5@ecu6vS|a_#B!%%{9g*r~K5;Mjewmr{8z#5Tq8XwW>kqCTy#a`Dt!7HV3w{wyw!5 z>_isp79qQ2@^il4UWbC)xz`zC-L*VU%$4+KM06q3;vjKa$&9FBl&Y2nix8)UPgdN6 zW?YT7*uWS6v};YixFFfJImr? wKqkp{2aB+t}D`BQR5jxZex&iW_XU zDRNRid)i;z*`JfZndQNorqcyVAqkfYcrTVL$JxN<1JZCTM4tdUv 8C@ss*%ayhOTaRr+2l-tIQwsC#K`2ApbZbWa}y$}EZPdT{92=38?d+xPLcUreC zwr;zomRolitveR_jn?jk^9yu=8m-UVY2CNjy6;v?x%H6IdZ;vfFVb}<5?hSK7Pgcl zJw~MGCU=v+$s3Wr(#Sm~)mt$TUO6&oL B=Q9`g@&37_|E9qbVP+Tnt_ z!|3ev=K#!brlZ#yjT1(#B15Y33GWObkhltPFX!n1;FzvVPF617;8`AP`m{Wzt)Q-y z1x+Mc)ruHYi!BbVsAjZBU}PDy(`rV0C%kPjylp|bwObEwD~J1yaKG;DXLd83M&qp& z^dYGE%fANjBH{2z9Q8EbrE+d)*+-k Y0hdh;p3n-L%e*f%A}{8n7F z0WZk#Tr+O?nBkgspA}vuS2&K$h31|>?@C!#E?Q=XpCe3fMnh`@Q}QX0Hz_owDOJle z{dMC4R;K6}+>0S;@c?= >l^CNl7Lu3jfR!a*x3VY#8%DfXCv`j z<>*3g*YDi$xr0B;qdTtiTxj4qpVDG(%efr~B>H^-rZ1;lNWvG8f Mjv!cJg=RCVRP75|GNq!m7A42I*1(7(sTUEjFNjR1IlS<%d)Y*5r)(O2uAvD4 zW~iEbr Cy3wqg|_j^|0*ny}BZ~^ry07$rNza`$|0xet*G_| (@4{?*_s?EAVtu&bLx TiL4DT@BNC6e7(*% N?dlicGB2v68g(^Y_Jf3;!8k zq4)J9#luP|c=x}kB3N=21#c^BFP@x9HtUKyNq%p>GV`0^y|L{DKzTm82;SlOnT$$| zsWQ2V$`B+-7D1P}jDZFlAhiKVeWU@L;#Z2RNn;Bf^BY6kVR`*-5@$i2`Beg)RraKX z#t;H m z5^)-dEK!~a`JDAQNp`#*GF #h?Vh^g0367>LJo1|8;hYl9OK2*h zlTy!COVK6B7lo93N@j1V;6LY~O6d&*9KmWF*`KWD2dnw=(R*w8qqTfiy{dsJZKp=7 zh($NxJYV3vo5+4ddV}-bzKF_*TEtvMW2z;D6DWQz$S9M%1Qbr!5e{b0SYzh(G0dRG z%$4KY |?x< V8U$x literal 0 HcmV?d00001 diff --git a/openai-translator/ai_translator/book/book.py b/openai-translator/ai_translator/book/book.py index b079357b..36979caf 100644 --- a/openai-translator/ai_translator/book/book.py +++ b/openai-translator/ai_translator/book/book.py @@ -1,9 +1,9 @@ -from .page import Page - -class Book: - def __init__(self, pdf_file_path): - self.pdf_file_path = pdf_file_path - self.pages = [] - - def add_page(self, page: Page): +from .page import Page + +class Book: + def __init__(self, pdf_file_path): + self.pdf_file_path = pdf_file_path + self.pages = [] + + def add_page(self, page: Page): self.pages.append(page) \ No newline at end of file diff --git a/openai-translator/ai_translator/book/content.py b/openai-translator/ai_translator/book/content.py index 623c7b2d..59af412a 100644 --- a/openai-translator/ai_translator/book/content.py +++ b/openai-translator/ai_translator/book/content.py @@ -1,77 +1,85 @@ -import pandas as pd -from enum import Enum, auto -from PIL import Image as PILImage -from utils import LOG - -class ContentType(Enum): - TEXT = auto() - TABLE = auto() - IMAGE = auto() - -class Content: - def __init__(self, content_type, original, translation=None): - self.content_type = content_type - self.original = original - self.translation = translation - self.status = False - - def set_translation(self, translation, status): - if not self.check_translation_type(translation): - raise ValueError(f"Invalid translation type. Expected {self.content_type}, but got {type(translation)}") - self.translation = translation - self.status = status - - def check_translation_type(self, translation): - if self.content_type == ContentType.TEXT and isinstance(translation, str): - return True - elif self.content_type == ContentType.TABLE and isinstance(translation, list): - return True - elif self.content_type == ContentType.IMAGE and isinstance(translation, PILImage.Image): - return True - return False - - -class TableContent(Content): - def __init__(self, data, translation=None): - df = pd.DataFrame(data) - - # Verify if the number of rows and columns in the data and DataFrame object match - if len(data) != len(df) or len(data[0]) != len(df.columns): - raise ValueError("The number of rows and columns in the extracted table data and DataFrame object do not match.") - - super().__init__(ContentType.TABLE, df) - - def set_translation(self, translation, status): - try: - if not isinstance(translation, str): - raise ValueError(f"Invalid translation type. Expected str, but got {type(translation)}") - - LOG.debug(translation) - # Convert the string to a list of lists - table_data = [row.strip().split() for row in translation.strip().split('\n')] - LOG.debug(table_data) - # Create a DataFrame from the table_data - translated_df = pd.DataFrame(table_data[1:], columns=table_data[0]) - LOG.debug(translated_df) - self.translation = translated_df - self.status = status - except Exception as e: - LOG.error(f"An error occurred during table translation: {e}") - self.translation = None - self.status = False - - def __str__(self): - return self.original.to_string(header=False, index=False) - - def iter_items(self, translated=False): - target_df = self.translation if translated else self.original - for row_idx, row in target_df.iterrows(): - for col_idx, item in enumerate(row): - yield (row_idx, col_idx, item) - - def update_item(self, row_idx, col_idx, new_value, translated=False): - target_df = self.translation if translated else self.original - target_df.at[row_idx, col_idx] = new_value - - def get_original_as_str(self): +import pandas as pd + +from enum import Enum, auto +from PIL import Image as PILImage +from utils import LOG +from io import StringIO + +class ContentType(Enum): + TEXT = auto() + TABLE = auto() + IMAGE = auto() + +class Content: + def __init__(self, content_type, original, translation=None): + self.content_type = content_type + self.original = original + self.translation = translation + self.status = False + + def set_translation(self, translation, status): + if not self.check_translation_type(translation): + raise ValueError(f"Invalid translation type. Expected {self.content_type}, but got {type(translation)}") + self.translation = translation + self.status = status + + def check_translation_type(self, translation): + if self.content_type == ContentType.TEXT and isinstance(translation, str): + return True + elif self.content_type == ContentType.TABLE and isinstance(translation, list): + return True + elif self.content_type == ContentType.IMAGE and isinstance(translation, PILImage.Image): + return True + return False + + def __str__(self): + return self.original + + +class TableContent(Content): + def __init__(self, data, translation=None): + df = pd.DataFrame(data) + + # Verify if the number of rows and columns in the data and DataFrame object match + if len(data) != len(df) or len(data[0]) != len(df.columns): + raise ValueError("The number of rows and columns in the extracted table data and DataFrame object do not match.") + + super().__init__(ContentType.TABLE, df) + + def set_translation(self, translation, status): + try: + if not isinstance(translation, str): + raise ValueError(f"Invalid translation type. Expected str, but got {type(translation)}") + + LOG.debug(f"[translation]\n{translation}") + # Extract column names from the first set of brackets + header = translation.split(']')[0][1:].split(', ') + # Extract data rows from the remaining brackets + data_rows = translation.split('] ')[1:] + # Replace Chinese punctuation and split each row into a list of values + data_rows = [row[1:-1].split(', ') for row in data_rows] + # Create a DataFrame using the extracted header and data + translated_df = pd.DataFrame(data_rows, columns=header) + LOG.debug(f"[translated_df]\n{translated_df}") + self.translation = translated_df + self.status = status + except Exception as e: + LOG.error(f"An error occurred during table translation: {e}") + self.translation = None + self.status = False + + def __str__(self): + return self.original.to_string(header=False, index=False) + + def iter_items(self, translated=False): + target_df = self.translation if translated else self.original + for row_idx, row in target_df.iterrows(): + for col_idx, item in enumerate(row): + yield (row_idx, col_idx, item) + + def update_item(self, row_idx, col_idx, new_value, translated=False): + target_df = self.translation if translated else self.original + target_df.at[row_idx, col_idx] = new_value + + def get_original_as_str(self): return self.original.to_string(header=False, index=False) \ No newline at end of file diff --git a/openai-translator/ai_translator/book/page.py b/openai-translator/ai_translator/book/page.py index df12e772..bcca7689 100644 --- a/openai-translator/ai_translator/book/page.py +++ b/openai-translator/ai_translator/book/page.py @@ -1,8 +1,8 @@ -from .content import Content - -class Page: - def __init__(self): - self.contents = [] - - def add_content(self, content: Content): - self.contents.append(content) +from .content import Content + +class Page: + def __init__(self): + self.contents = [] + + def add_content(self, content: Content): + self.contents.append(content) diff --git a/openai-translator/ai_translator/flask_server.py b/openai-translator/ai_translator/flask_server.py new file mode 100644 index 00000000..7c87d74e --- /dev/null +++ b/openai-translator/ai_translator/flask_server.py @@ -0,0 +1,71 @@ +import sys +import os + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from flask import Flask, request, send_file, jsonify +from translator import PDFTranslator, TranslationConfig +from utils import ArgumentParser, LOG + +app = Flask(__name__) + +TEMP_FILE_DIR = "flask_temps/" + +@app.route('/translation', methods=['POST']) +def translation(): + try: + input_file = request.files['input_file'] + source_language = request.form.get('source_language', 'English') + target_language = request.form.get('target_language', 'Chinese') + + LOG.debug(f"[input_file]\n{input_file}") + LOG.debug(f"[input_file.filename]\n{input_file.filename}") + + if input_file and input_file.filename: + # # 创建临时文件 + input_file_path = TEMP_FILE_DIR+input_file.filename + LOG.debug(f"[input_file_path]\n{input_file_path}") + + input_file.save(input_file_path) + + # 调用翻译函数 + output_file_path = Translator.translate_pdf( + input_file=input_file_path, + source_language=source_language, + target_language=target_language) + + # 移除临时文件 + # os.remove(input_file_path) + + # 构造完整的文件路径 + output_file_path = os.getcwd() + "/" + output_file_path + LOG.debug(output_file_path) + + # 返回翻译后的文件 + return send_file(output_file_path, as_attachment=True) + except Exception as e: + response = { + 'status': 'error', + 'message': str(e) + } + return jsonify(response), 400 + + +def initialize_translator(): + # 解析命令行 + argument_parser = ArgumentParser() + args = argument_parser.parse_arguments() + + # 初始化配置单例 + config = TranslationConfig() + config.initialize(args) + # 实例化 PDFTranslator 类,并调用 translate_pdf() 方法 + global Translator + Translator = PDFTranslator(config.model_name) + + +if __name__ == "__main__": + # 初始化 translator + initialize_translator() + # 启动 Flask Web Server + app.run(host="0.0.0.0", port=5000, debug=True) \ No newline at end of file diff --git a/openai-translator/ai_translator/gradio_server.py b/openai-translator/ai_translator/gradio_server.py new file mode 100644 index 00000000..e05ef3a5 --- /dev/null +++ b/openai-translator/ai_translator/gradio_server.py @@ -0,0 +1,54 @@ +import sys +import os +import gradio as gr + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from utils import ArgumentParser, LOG +from translator import PDFTranslator, TranslationConfig + + +def translation(input_file, source_language, target_language): + LOG.debug(f"[翻译任务]\n源文件: {input_file.name}\n源语言: {source_language}\n目标语言: {target_language}") + + output_file_path = Translator.translate_pdf( + input_file.name, source_language=source_language, target_language=target_language) + + return output_file_path + +def launch_gradio(): + + iface = gr.Interface( + fn=translation, + title="OpenAI-Translator v2.0(PDF 电子书翻译工具)", + inputs=[ + gr.File(label="上传PDF文件"), + gr.Textbox(label="源语言(默认:英文)", placeholder="English", value="English"), + gr.Textbox(label="目标语言(默认:中文)", placeholder="Chinese", value="Chinese") + ], + outputs=[ + gr.File(label="下载翻译文件") + ], + allow_flagging="never" + ) + + iface.launch(share=True, server_name="127.0.0.1") + +def initialize_translator(): + # 解析命令行 + argument_parser = ArgumentParser() + args = argument_parser.parse_arguments() + + # 初始化配置单例 + config = TranslationConfig() + config.initialize(args) + # 实例化 PDFTranslator 类,并调用 translate_pdf() 方法 + global Translator + Translator = PDFTranslator(config.model_name) + + +if __name__ == "__main__": + # 初始化 translator + initialize_translator() + # 启动 Gradio 服务 + launch_gradio() diff --git a/openai-translator/ai_translator/main.py b/openai-translator/ai_translator/main.py index 6b8e0c9b..88d5acd8 100644 --- a/openai-translator/ai_translator/main.py +++ b/openai-translator/ai_translator/main.py @@ -1,27 +1,20 @@ -import sys -import os - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from utils import ArgumentParser, ConfigLoader, LOG -from model import GLMModel, OpenAIModel -from translator import PDFTranslator - -if __name__ == "__main__": - argument_parser = ArgumentParser() - args = argument_parser.parse_arguments() - config_loader = ConfigLoader(args.config) - - config = config_loader.load_config() - - model_name = args.openai_model if args.openai_model else config['OpenAIModel']['model'] - api_key = args.openai_api_key if args.openai_api_key else config['OpenAIModel']['api_key'] - model = OpenAIModel(model=model_name, api_key=api_key) - - - pdf_file_path = args.book if args.book else config['common']['book'] - file_format = args.file_format if args.file_format else config['common']['file_format'] - - # 实例化 PDFTranslator 类,并调用 translate_pdf() 方法 - translator = PDFTranslator(model) - translator.translate_pdf(pdf_file_path, file_format) +import sys +import os + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from utils import ArgumentParser, LOG +from translator import PDFTranslator, TranslationConfig + +if __name__ == "__main__": + # 解析命令行 + argument_parser = ArgumentParser() + args = argument_parser.parse_arguments() + + # 初始化配置单例 + config = TranslationConfig() + config.initialize(args) + + # 实例化 PDFTranslator 类,并调用 translate_pdf() 方法 + translator = PDFTranslator(config.model_name) + translator.translate_pdf(config.input_file, config.output_file_format, config.source_language,config.target_language,pages=None) diff --git a/openai-translator/ai_translator/translator/__init__.py b/openai-translator/ai_translator/translator/__init__.py index 7d6acacd..f218c2a9 100644 --- a/openai-translator/ai_translator/translator/__init__.py +++ b/openai-translator/ai_translator/translator/__init__.py @@ -1 +1,2 @@ -from .pdf_translator import PDFTranslator \ No newline at end of file +from .pdf_translator import PDFTranslator +from .translation_config import TranslationConfig \ No newline at end of file diff --git a/openai-translator/ai_translator/translator/__pycache__/__init__.cpython-311.pyc b/openai-translator/ai_translator/translator/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a7cb25e5d62c98db0e88a8e560bbf75cc3ea1e9 GIT binary patch literal 349 zcmZ3^%ge<81l7yFrp5s2#~=<2FhLog<$#Ro3@HpLj5!Rsj8Tk?AU0DDQ!aB9Gmy<3 z%%I8gk`bs#lkpaBfQws5QDR >plbI#98%S`vvWVyvxkdhW(0#{eW z4Agx~2qpkj8lMbSTf_pCSjq4iNHTnl2NJGUG0)~TK3%uSJs>0|zaTX)F;lm&G&4E7 zxFoTtBqk>@FFiRUF*6S)2QxCiC?+uz$;KEYiTL=;yv&mLc)fzkUmP~M`6;D2sdh!6 hfB|``SRY7yU}j`wyuqM(0Tta~FuZ^b6>$Ld004iDY25$- literal 0 HcmV?d00001 diff --git a/openai-translator/ai_translator/translator/__pycache__/exceptions.cpython-311.pyc b/openai-translator/ai_translator/translator/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f48568cdece94e17f997e399acb7a92567208bf GIT binary patch literal 981 zcmb7C&ubGw6n?YWm~BkxLC{u;4&o)CfnJM NluR zVpr%r-`}QNrlj2hZDLy9pc^=$dof|O%>}1|w?ZxhZkJ1Xl;6N5ES?+9PcxH1R)$S3 zB7-r)?;%O6=6{S^iT2y#0@7H)eRM_{LdZzdiR%4t PeBu0zGr!Kr$4&kMebf7~ literal 0 HcmV?d00001 diff --git a/openai-translator/ai_translator/translator/__pycache__/pdf_parser.cpython-311.pyc b/openai-translator/ai_translator/translator/__pycache__/pdf_parser.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d3768936fee6d4d05c6e4a4c7d12497f64fb906 GIT binary patch literal 3365 zcma(TT}&IvdG^m<|C_KObwD|YZMhDD16R$ZBu6h7Luhp+Q6W|FK~{@*Atvj!Z+E!_ zx~{k?mGXcp-{~Zfs#ezZ#6gqm UO(*;$Zm60RKJ3>mSt?8+l# 2!mW>n>FPa#n(I+ zfkFO}Y~)0aF_<{W6LW9pY`UkRe+!|H>eHOvuoaqosV{&wJ{IwzJ9)gd@i`1f9LxoC zPQ!`Db2*pcLSrE3HrN6_(qqkVi-bsSq4+ O@6X^bWIMOvP%rD@8W+kkM=M9J NraRtFJ(CB;QeQE~WBM2~qst*AS3fHGe2)@JDucbKQ+k!P8-bP>p zECK_}UC0pzQ9$$Ktu+I_>3TE1Ps#QEDWKvrLq(6#bTyTYl8_u6g0!4cBn{vG-d+_S z&@1N^Ii=~zOgcNd8fnxhtAz2W5)EFFGf6?#MiEkDjG~QN41qOZ4lGpru5QlE?aw6l zW|GAjsWc-Y^0xTR(%+YMN5Orh6j)%~F=+~alW zzCP#dU1dMi@k2A`jB+ZD9&{q3vhs3zUQ!{Ai#DWWNl{OuH5Fe!mBCzYv|SaFx+MpC z>oY7%gLYNQ%0g0tPAw~^bSo&3Udp5t)rLjjaslb-vENQZmmrG#k) r&K@Q$zgzPf%H6nLMdb|^jamH*E7|Z0kXKlV;ZC_$krT&Hp^KJI<1ix z1zmuYnz;w*q$JDGTVv#_CU+%eNl+w_Z=?I$>ClbiS@Q!jRW+xOoxR7Jl^4)a&w$m= z6Vk$swrbZ 3ka4x&8R|Uih*ZzI@O%@OAP@@M&~Md93V8#jbd%D{lJZ&wSj&T)8jwWgzd(d!J!s z`{MT4_Qg_P49$BkR6wsM^a6uCdvK=v(~m#@I8Rq>peMB7J+#+7RP2tGx?_2^9O&B* z4DAJmih)=u5X;*TPcT{mRL2-mh528W-$DfPG$vrUNtHv93LpZQfZ=Am9C>>`^3Go5 zon3l&{@cD{BwmWdHzzhHDwM@WS-YKp>!|>?<9*^;cwj$#X)k=K7#=BwNAi yCW; zdvpnk0}CZI<1UmV{a>wov0@HiFGeOxkqMIvJr7>^r>k^n!kkK)L9rMVOF=RJL7Dr} zJ{Q{KLWS8P7b|fwlZzd2XFmUhdG0-Pa1_IH?-jYLCGM)pU9BK9&SKYoa?tbR?XF_a zP^o7qKh_X(tH|}2xPFuCKN2#$9X8Jmqh6Q(%+GCjKJ#pfMSrm551RfULMm4Y6z)nV zI#BCWE${-L@AnuqXeVJWU%otvT=@+F{M
r8TR-L1tM`$(mJ+s=nY#-|+7hd!-ts zQFR t*mWv$dDOX|uIgY%u}_?>KmYY2Tl) HU^(%BD;+95 literal 0 HcmV?d00001 diff --git a/openai-translator/ai_translator/translator/__pycache__/pdf_translator.cpython-311.pyc b/openai-translator/ai_translator/translator/__pycache__/pdf_translator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5dc8fb9f7a477257769ccecd9971bbc776e6a07d GIT binary patch literal 2363 zcmZ`)-D}%c6u+`0Te9O=?VN0FLlbD`#m$oJi_tb)7ihcgp>qQ5TIQt)%~BF2vgBUL z>)JZdK_4>6LkEFwtPHb gadQ zx%b?2&OP_)mt-=Ipxs)3ZYUx`e^AD6LI>>50=S2Cq;nQ3a(RxUIATSLe4b~Rw}hgY z7a10;Xfc+LF)UhAQO?U8ilFmIkKRFgY(0X|2E6T-kL%J!l$HOYY?dR^bjdRvTeASm zGiN`Z(XeY^V4_#B=`o~S!kX<`8Xb3LK{IX8@)xGh7dj@{r>Dd0UI1_p87R-`C?C-| zgP)7&kvl?OkWu^5lX}eHbqQV>-Z;DoLo}j?0(*0Dz2}aYkLgixwU;EpFW(0TlX%h5 z4NJAPqEQIqg1LO>-Mz_N@VLMps&MPTZ3L1EQUmP4F8FO7z=qEt7)5Qe=E*)nXy2&7 zf{rethg>cj#ZeeeQeG!>hFYMB!*qj5yrj>mC6;GQXOigkcB&&pa1Cn?(*kj6WJ5#G z4fvcpHT&mJ-#q*E=kqg{W}T8@Yv$OsvRPPiJq>%amS)d0^S;hs)xooxsdjN)kovI4 zj+br_NmWhT^i=g`e< `bQRp3+ybq>deBZ<>+!OG&i;w6wAIDpRpOzvJ5K( zu~fTO_B{Y^ m3So)&I^ZDFRzwh zmEcHJ*_HO<)NVg2k+6-62-6!Qo4~#BAu>A-DikFVzI2==A{%zOXkZO)pd)mERB&w1 zuss(iDa+%s4RrxE;MXwRO*lnWX;=Ho2*if2QhV7Rtbh`~O37=K(6I4gO4{~m1*xX~ zH4!KxiOzaV6zeb~(X|?h!s+wMu1n(>>T37^2uRW&YS5@|9tj_*kdj8^{to0eYW1SN zLu=F9>9MW!*n`C WLSKk?V zdvo~lSbb!wF*4OcLXxT}tt-QK6W?EXnmOLcoZ8Nu-pZVQa{aed?em3t#%yHFx^fKk zm!B%>HF+&nl^RO6p}ey>*-$>(R;IU<>DtT}wX21?qBj)1#(GO;=6jHmUcI%Qn%GKB zYzmL%+T^G8RIZWA)ubGL1tQ7{I1L}%M@c`BTvnnN4ZXWnmBdvQ?((ulagVBAD{EFq z!@C~F2k9(Y-h?-84<85g5HY(f$C%b1BlL*4Yd~%XPb DKJS<|g=J0DW=^pvEW9-0g1LwLUmI_sp3& zXU?3NbLN} %#sQhV=!Xq9(D>xw8wHvk0R+XP`W9qzwTJhKR*6(U4|f??w#z zark8T5?BBUKPDT=88II-Qn0s7*pVZe qOi6TN=0Qhqvd#r0p8mQXS34Vf a$!$7<+(o5vD)ylq)=BmMKv;x>EhtPbnFzxCuxbWc kUl7>!TkH{* z!i23)*qsbT(;0IK%d}9^BWy^7qKB=qx?jxOP;OWaySxYz)CFs5-H)(sf`LJE@o-JO z-=eh!?9+uQi@G8k^_VZsklmYr-kzODZ+`Upjjui#KA1b|0)%Pi!zX-Gf5n3U(4#CS zk-rUDBWql8R5R5jy%ksh4mFxQP?!p3RW%)xs_Jx4BlZno8EA;uF?c@$IgM&EpLvR_ z2`I7ZUJ`BCRZ;fMJvgt7RFsiGUSEr$-u`mi^}fMM-|$@9eBa?p-{C-BcQ1yNZI!+! ztC{{_(~clLcw69= Z<)|AJU&+eSo}tgs1@%>cS9f+@h9v=Vd`MFU<8 zoj(GwRFYnkMi$yhZW5goj-W{nG^8;eAS-qtL~#E$flp*JBU^w>Z`tiAm=KpJuCDup zU_&u{0vOc9CCHAQ%HCX7A`Fc~f%s0C*mF|H1@ #7^KL9NBb z&22RV M*f2y123(LGDc#Xw%`v3<_mzY%?!+`U@(vUhhzxEAMty_S)H9?0?} z4H~&@oZUC}K&xt)R8@GeeT(sFRXyQr)`CRvC<(<;*R@DLOpA`?b3`^WJ~}7%Xc9&k z)_RjbPB(uwzE^sQt06`&JX&wBY_GQ~&>d-M-8rF#pt+<+&DEi(nHvdSd};2vi2qMg z_lbFOfc-UT!jf8ttO#kB3=c%G L2*v7)1a8 literal 0 HcmV?d00001 diff --git a/openai-translator/ai_translator/translator/__pycache__/translation_config.cpython-311.pyc b/openai-translator/ai_translator/translator/__pycache__/translation_config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eed820257dc0197b10a0092debdb74ac2157779a GIT binary patch literal 2359 zcmahKTSy#N^xm0Sops%{YE99u?c`D2{cvMR3$-y)F^{%TY0w0+*bd{&tQj-Tnme-^ z?JjIfAZrjB+7t^VW$A}i64HM9*L?PKH%y5c5DKM*{%tS;$yd*v*=MxvotZQD+;i?Z z_dM^9!C(u5@!9$_wT(mQ4~kR<;O^Lsz;6vHNWnUaBL ^g(cUHH5a#|oZ6 zQSNVwj?3j$_gCTBjRATCWT^ <2O#${Y5o}QD1Z}%$z-{3WrMzd9NcF3Z%@~|p40iH&U=g-&iRrp;l z&~>+}FO9@1QEAX5R%O@gpFo><9E|XbnqgX!A*+thOwFr=P ^FQzZaPH&DspPzBNZP>FlqO#R(Zrh4C1Xau zENMoCW|cjcBvX -yG=ZRu?$PN!Z3XeVvk3KkB44<{bXLDn#A8j9t(4XCUc)P7*HB;cjyC49`mF%Dh zmFvc`4?KQ@TSHdW^AHO>Tg4RI7`?YqhOvB9gg0iZ(M&Vzu&Wlqn;c{^JdOJ?G6F}@ zo5 >$vt)i!2z%%R$m#6mb8)>#t zIz}rT o|Gls9EsI-w1WimEdY=|CB*c$)WG9c-}v@K-nV5wNNi=cGKKRO z3+JbEW4F5C=e;BZ8>4=QqZQx;ywiyZO;;UmNt)9gzbPeDQBO*W!!1a}bbOkn&Y4bA zYF?2nl~7L-+W4R_IaAdW4kwWr)4>U+J-Gn2sVRzTh_3Y}IRuO*4KUa)yyqJ4wA9R7 z4*!?&t#tEa0AT1&u>ICCyW>cHtQd^g!AJ>VfB(klPFMHsPxGV2u0Fe~FL$=Y!m3Mh z!4t~#l!DsYL9@NRy@QrZe1mgcH}6CTHk8etdp(b$Ll2`v#pp>pda}d<;}aT+rxXC_`R{BRHHP&8c3a7Q6%r zh~kWDQAwh>+`CUD^wBDr+F`yAAd7wvw5^WVfx|SCgL!2`xo6n1(}h0zZg;+Nd&KUH zZXB~aj~7~x?}WmI-pOAl0eEjQbioc?DDW4&`rsWNnl+pYZkHBOoJ%Sxoz7cD@oGxa zJ&I=<=KAes;Y6|0R Y6v( PcQ2+n{ literal 0 HcmV?d00001 diff --git a/openai-translator/ai_translator/translator/__pycache__/writer.cpython-311.pyc b/openai-translator/ai_translator/translator/__pycache__/writer.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9646147427cef85317c8dddc4e4c7b160d03c37 GIT binary patch literal 7339 zcmcgxdu$s=df(+s%a IzU z*Y6uXM9PwJEe;(me>3~dH#6VNelzo%(WiF1g@N$V^=HDr*D%a~;7dLja*3xYNL*!j zhG#>J4`W@34eNZmu->N+8+-;9W%`gYZ1S1HWxg`{ZU}K jmEkI1Rd}6m9nCKbRflVQHQ`!cEq&)g>%(=vI+oEfgA8xJ#PF8u zI)=FkzijY1cy@&GSRdo-ut%?%#-gDp5jFF4U_uasaY59Ku}Dx7Jvyy=LXbuR;pvbt zB3%GDCkT>Oek?!&6C^NwjtV`jW|`*4!-7PDV |7ui$>&KV5D{UdqU1~<=r4LJN}i_{ zbxdE-IKMv#pUUr#S7goX$P3D$ZLtjCB9rE9mkm!L$!LbNIFwb<*mO+tj|W47e>_UU z0ci|-lMm>~w08s0jjN1Q)bq(gI-$D`>6?XjmPzPR1t-f_h4FRx-7E-rc8Y3|^$8|b z+-gB1!IsvX)JF#Y$hvh!b?2vJp o5Q5_rLmb>1RLb zc56(`4Y|Mn^_Sn=`svc=GmkFM{^#A9tj^cJ{$%NwbKiXY58ZB08CeHvnvoaI#wIjl zD0&`dQ@vjdd>|A&N8tVV&HdQ;tE*?_P11}4AyJ|kf|2p4$3PrVIdLLvz-B6=F{$7- z(*fz6h(qj#K +X4LQGDEwc)WiJt>pWFB(X8MC~uQQ?|Yu1V&a7Ol<)){P6+ zjf%BNwKmNg7p(2FwLR@>eBj!%;M#MCS6qFnt53E$;Yr(HePC}|us1387S-M|-?m`i zA=`IQ?cN1f@BJN$>!9j7nAKjgRm=>@>-*#_2N2iyDK@Wa^U5}Fy1I6D+Y;Eq$yEdL z4liQWfWi%`+@Q=2E>^f?&P5u) p~cj&zN4)7}Y+9U|FAyt?sc?rfm%j-%P z=cG<5cp$?}7Usv=-k8)&=H)sk3#BFdvJ^eDw4Kr!#xhbxu{LkyO?=sB+;#ZCHw*J9 zX^^moxscFF=zO^_Nw-CO`OI9}Qqm})r{_ZHT97nNFv&6)CtE(>%gnOx4koz-H(9Ws z%FIb~!aRwK8V%$R@K&%{a_7VlO%`m-tp)9%S+F!|ky@8^6PAQwg|*g1Sqe7x7em6j za?ECqtZ74SzI@Jx`4Ss!QhPohe89q6d7IR+ESbc4M+59u_7`$-uH|#f`3k<0uYx+( z@zo`Dyi%l9!`DJB)%^Mr4Rm4}#*Gu^(!M3_(wq5QSTk*tIR9x#y_!!I$E9C&MP22I z@|3GsJD*P2OEqB!S!?XeS8C3(Gu8xG>^Xd0VPKdkI&a8pYt<(m$lI`TcUq;Lp@cE@ z+Oqy~Dp9slJE4P>-ZA{#&;hG$yrO6gM!_@m-le-2mp=W;(yhBoAOB0YJ6_$<(K!yh zM(h-WVKElzkfbr-MZxe$Eb S@f?F+x8@k7TfI!old9?rJXy4HzN8j-nG~VI(*=*eyT)vq6&I&K5JyiSDe+E&9e6%9UVRT*0BR#@6hmIQBidD zoN147StQ-G0O`T>zPy08pcw|=8S;|-C`6+?8#OzOWR|($7NnIC0eB^_NSxvX=QiO0 zG#e2nf+FxXWTFIDNP=XD;5tQcNz+UrL4tgO`?qGK SiC+TR zNpNk^3^;n4HP6R_(Fn293Io$X?|7iI2YQe66Z~)l9Z@q(5aEJmIe!j#q!^A)37YY& z5S+jWOrIjS(rKm-0-+dCZ<7=af#Dh_0`o@BO>mh5)mSjZdn$@HW!!%>Gx~*}4uvi2 zLVgiTX?i{ioL*QY*`KNi#^ohY|`&Kr>2|gIj5otHO*!O$8#_fIEFr zpqoBvf{eI3&plV$ r%Ncnd{1!nA*0>=7-!mm2>@_DfQv)dd0n4b?;WV9+m5nxt?bx#?g{G zcgOKnuYA%c*PT}CPOEjNFWVra!Y#YE+ ?3)>`gbguIQ}Dp%`kOUTheu}s&%jb@*}l# z-$LC!xo+R0t3`Efn=?FgZJMu{PbjV))zvd+NIN$Ct?jREsZqtzraIbW`YhJF)%x}? z8q}@53-!Hneea^XRdsKlGd^tDlHTB%-+6o2y@9)jW#_^4hE4M=w_ES+RCgbdoo}U` z2hz^Iv~y3|*_%~-S1>&uqnzx`jUo(VY0yYsjMn#Oc*C|X`x}3B_?>sC5VRAIZ}=lxIq(F4jPiCgg#Z^ zL3t_03E*OJh*$15GyB(Uslc6bE!{0?sf_1%a|+j|7Xoj&1iSDuv+}Muy_MR4R`FJf zHpC?NlA7%ktW>m*7PqpZRv7gal9lb4dD|NHms9%-957X2e#Koa QQWECLW)N!Z`M5DAjkmvnOhSl^MUhp+br#EWUBdlb z?@qW&Za;AQ+z!8|I{*$t3ovV$cdL_Z@kx5+N&7f+6)c*m!MhT&9tz%oI~-9WaKEGL z*g`L%V&RC0ge7;Nd~WWU5F)~d(`0Y_jhF5)`2%c6PbfMT2#I?;3MJ1$eIo9Z-@xBR zM&9X%=l%y@h Z5=BDEnTF^l C9ZF8Z!hUOPV{Q{Jv|9 $OB>1 7yun)JdO=|d=xRn34`f@A=Ng9rx^h7fQK zBZm+U18Bzc^wRGw6c7{ujSJz7(expx0eeA406f-}6wz9AZ$nO_-WdeEoFsk#&2$dV z5pZ*476s&jKo#+xkiQ^WA=kYevW%}8gz@G-YRpf7s`v(_eL$v4Xx~w#;h5TR?4`8t zp}k68w=vasyH&P#EB0>H-ko7;&0FtGEjpanEE%TSzV!*htN|{1t|c=}dDRg6gyQTV zaI`Bwe8SjkhuCz(Mzx{yi{0w>zJ-Q9xuI{du}y8 {puo^4W2vd0Y;LmFDo=Kzh^W`Pi-aFXDH)?zH~4?W|# +Hx{&19!!G*S8 zv-$n+#{!{j4tWKtAe9I~1P?+J0y;9OL%^$$taI033IQ3Ecm?2M;V+|OjrI&v0F&M5 zp0CL;5Z!O2(MoV{H@Y(nz&uZ*`(rd(2_d!t^#E?~pwU;`X|yI}4MsCP^_ng!YIZmU zM~M^)ob3n&&(c#TJ>(LgQgAU_lG~9z6Vh`pJriqH%lU9tI~x#%>>0ZL1vy+#(Ax@n zpw`wE<->n0E= Book: - book = Book(pdf_file_path) - - with pdfplumber.open(pdf_file_path) as pdf: - if pages is not None and pages > len(pdf.pages): - raise PageOutOfRangeException(len(pdf.pages), pages) - - if pages is None: - pages_to_parse = pdf.pages - else: - pages_to_parse = pdf.pages[:pages] - - for pdf_page in pages_to_parse: - page = Page() - - # Store the original text content - raw_text = pdf_page.extract_text() - tables = pdf_page.extract_tables() - - # Remove each cell's content from the original text - for table_data in tables: - for row in table_data: - for cell in row: - raw_text = raw_text.replace(cell, "", 1) - - # Handling text - if raw_text: - # Remove empty lines and leading/trailing whitespaces - raw_text_lines = raw_text.splitlines() - cleaned_raw_text_lines = [line.strip() for line in raw_text_lines if line.strip()] - cleaned_raw_text = "\n".join(cleaned_raw_text_lines) - - text_content = Content(content_type=ContentType.TEXT, original=cleaned_raw_text) - page.add_content(text_content) - LOG.debug(f"[raw_text]\n {cleaned_raw_text}") - - - - # Handling tables - if tables: - table = TableContent(tables) - page.add_content(table) - LOG.debug(f"[table]\n{table}") - - book.add_page(page) - - return book +import pdfplumber +from typing import Optional +from book import Book, Page, Content, ContentType, TableContent +from translator.exceptions import PageOutOfRangeException +from utils import LOG + + +class PDFParser: + def __init__(self): + pass + + def parse_pdf(self, pdf_file_path: str, pages: Optional[int] = None) -> Book: + book = Book(pdf_file_path) + + with pdfplumber.open(pdf_file_path) as pdf: + if pages is not None and pages > len(pdf.pages): + raise PageOutOfRangeException(len(pdf.pages), pages) + + if pages is None: + pages_to_parse = pdf.pages + else: + pages_to_parse = pdf.pages[:pages] + + for pdf_page in pages_to_parse: + page = Page() + + # Store the original text content + raw_text = pdf_page.extract_text() + tables = pdf_page.extract_tables() + + # Remove each cell's content from the original text + for table_data in tables: + for row in table_data: + for cell in row: + raw_text = raw_text.replace(cell, "", 1) + + # Handling text + if raw_text: + # Remove empty lines and leading/trailing whitespaces + raw_text_lines = raw_text.splitlines() + cleaned_raw_text_lines = [line.strip() for line in raw_text_lines if line.strip()] + cleaned_raw_text = "\n".join(cleaned_raw_text_lines) + + text_content = Content(content_type=ContentType.TEXT, original=cleaned_raw_text) + page.add_content(text_content) + LOG.debug(f"[raw_text]\n {cleaned_raw_text}") + + + + # Handling tables + if tables: + table = TableContent(tables) + page.add_content(table) + LOG.debug(f"[table]\n{table}") + + book.add_page(page) + + return book diff --git a/openai-translator/ai_translator/translator/pdf_translator.py b/openai-translator/ai_translator/translator/pdf_translator.py index ab0b40b4..7a53118f 100644 --- a/openai-translator/ai_translator/translator/pdf_translator.py +++ b/openai-translator/ai_translator/translator/pdf_translator.py @@ -1,26 +1,29 @@ -from typing import Optional -from model import Model -from translator.pdf_parser import PDFParser -from translator.writer import Writer -from utils import LOG - -class PDFTranslator: - def __init__(self, model: Model): - self.model = model - self.pdf_parser = PDFParser() - self.writer = Writer() - - def translate_pdf(self, pdf_file_path: str, file_format: str = 'PDF', target_language: str = '中文', output_file_path: str = None, pages: Optional[int] = None): - self.book = self.pdf_parser.parse_pdf(pdf_file_path, pages) - - for page_idx, page in enumerate(self.book.pages): - for content_idx, content in enumerate(page.contents): - prompt = self.model.translate_prompt(content, target_language) - LOG.debug(prompt) - translation, status = self.model.make_request(prompt) - LOG.info(translation) - - # Update the content in self.book.pages directly - self.book.pages[page_idx].contents[content_idx].set_translation(translation, status) - - self.writer.save_translated_book(self.book, output_file_path, file_format) +from typing import Optional +from translator.pdf_parser import PDFParser +from translator.writer import Writer +from translator.translation_chain import TranslationChain +from utils import LOG + +class PDFTranslator: + def __init__(self, model_name: str): + self.translate_chain = TranslationChain(model_name) + self.pdf_parser = PDFParser() + self.writer = Writer() + + def translate_pdf(self, + input_file: str, + output_file_format: str = 'markdown', + source_language: str = "English", + target_language: str = 'Chinese', + pages: Optional[int] = None): + + self.book = self.pdf_parser.parse_pdf(input_file, pages) + + for page_idx, page in enumerate(self.book.pages): + for content_idx, content in enumerate(page.contents): + # Translate content.original + translation, status = self.translate_chain.run(content, source_language, target_language) + # Update the content in self.book.pages directly + self.book.pages[page_idx].contents[content_idx].set_translation(translation, status) + + return self.writer.save_translated_book(self.book, output_file_format) diff --git a/openai-translator/ai_translator/translator/translation_chain.py b/openai-translator/ai_translator/translator/translation_chain.py new file mode 100644 index 00000000..79d6fd04 --- /dev/null +++ b/openai-translator/ai_translator/translator/translation_chain.py @@ -0,0 +1,54 @@ +from langchain_openai import ChatOpenAI +from langchain.chains import LLMChain +from langchain_community.chat_models.zhipuai import ChatZhipuAI + +from langchain.prompts.chat import ( + ChatPromptTemplate, + SystemMessagePromptTemplate, + HumanMessagePromptTemplate, +) + +from utils import LOG + +class TranslationChain: + def __init__(self, model_name: str = "glm-4", verbose: bool = True): + + # 翻译任务指令始终由 System 角色承担 + template = ( + """You are a translation expert, proficient in various languages.When you translate something,you can only return the contents you translate, + without any unnecessary words. \n + Translates {source_language} to {target_language}.""" + ) + system_message_prompt = SystemMessagePromptTemplate.from_template(template) + + # 待翻译文本由 Human 角色输入 + human_template = "{text}" + human_message_prompt = HumanMessagePromptTemplate.from_template(human_template) + + # 使用 System 和 Human 角色的提示模板构造 ChatPromptTemplate + chat_prompt_template = ChatPromptTemplate.from_messages( + [system_message_prompt, human_message_prompt] + ) + + # 为了翻译结果的稳定性,将 temperature 设置为 0 + # 默认chatGLM,若传入openai则用ChatOpenAI,记录选择的模型 + chat = ChatZhipuAI(model_name=model_name, temperature=0,do_sample = False, verbose=verbose) + if model_name[:3] == 'gpt': + chat = ChatOpenAI(model_name=model_name, temperature=0, verbose=verbose) + LOG.info(f"选择翻译的模型:{model_name}") + + self.chain = LLMChain(llm=chat, prompt=chat_prompt_template, verbose=verbose) + + def run(self, text: str, source_language: str, target_language: str) -> (str, bool): + result = "" + try: + result = self.chain.run({ + "text": text, + "source_language": source_language, + "target_language": target_language, + }) + except Exception as e: + LOG.error(f"An error occurred during translation: {e}") + return result, False + + return result, True \ No newline at end of file diff --git a/openai-translator/ai_translator/translator/translation_config.py b/openai-translator/ai_translator/translator/translation_config.py new file mode 100644 index 00000000..c5bc1700 --- /dev/null +++ b/openai-translator/ai_translator/translator/translation_config.py @@ -0,0 +1,29 @@ +import yaml + +class TranslationConfig: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(TranslationConfig, cls).__new__(cls) + cls._instance._config = None + return cls._instance + + def initialize(self, args): + with open(args.config_file, "r") as f: + config = yaml.safe_load(f) + + # Use the argparse Namespace to update the configuration + overridden_values = { + key: value for key, value in vars(args).items() if key in config and value is not None + } + config.update(overridden_values) + + # Store the original config dictionary + self._instance._config = config + + def __getattr__(self, name): + # Try to get attribute from _config + if self._instance._config and name in self._instance._config: + return self._instance._config[name] + raise AttributeError(f"'TranslationConfig' object has no attribute '{name}'") \ No newline at end of file diff --git a/openai-translator/ai_translator/translator/writer.py b/openai-translator/ai_translator/translator/writer.py index 12b37e75..9a2d4a81 100644 --- a/openai-translator/ai_translator/translator/writer.py +++ b/openai-translator/ai_translator/translator/writer.py @@ -1,108 +1,114 @@ -import os -from reportlab.lib import colors, pagesizes, units -from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle -from reportlab.pdfbase import pdfmetrics -from reportlab.pdfbase.ttfonts import TTFont -from reportlab.platypus import ( - SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak -) - -from book import Book, ContentType -from utils import LOG - -class Writer: - def __init__(self): - pass - - def save_translated_book(self, book: Book, output_file_path: str = None, file_format: str = "PDF"): - if file_format.lower() == "pdf": - self._save_translated_book_pdf(book, output_file_path) - elif file_format.lower() == "markdown": - self._save_translated_book_markdown(book, output_file_path) - else: - raise ValueError(f"Unsupported file format: {file_format}") - - def _save_translated_book_pdf(self, book: Book, output_file_path: str = None): - if output_file_path is None: - output_file_path = book.pdf_file_path.replace('.pdf', f'_translated.pdf') - - LOG.info(f"pdf_file_path: {book.pdf_file_path}") - LOG.info(f"开始翻译: {output_file_path}") - - # Register Chinese font - font_path = "../fonts/simsun.ttc" # 请将此路径替换为您的字体文件路径 - pdfmetrics.registerFont(TTFont("SimSun", font_path)) - - # Create a new ParagraphStyle with the SimSun font - simsun_style = ParagraphStyle('SimSun', fontName='SimSun', fontSize=12, leading=14) - - # Create a PDF document - doc = SimpleDocTemplate(output_file_path, pagesize=pagesizes.letter) - styles = getSampleStyleSheet() - story = [] - - # Iterate over the pages and contents - for page in book.pages: - for content in page.contents: - if content.status: - if content.content_type == ContentType.TEXT: - # Add translated text to the PDF - text = content.translation - para = Paragraph(text, simsun_style) - story.append(para) - - elif content.content_type == ContentType.TABLE: - # Add table to the PDF - table = content.translation - table_style = TableStyle([ - ('BACKGROUND', (0, 0), (-1, 0), colors.grey), - ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), - ('ALIGN', (0, 0), (-1, -1), 'CENTER'), - ('FONTNAME', (0, 0), (-1, 0), 'SimSun'), # 更改表头字体为 "SimSun" - ('FONTSIZE', (0, 0), (-1, 0), 14), - ('BOTTOMPADDING', (0, 0), (-1, 0), 12), - ('BACKGROUND', (0, 1), (-1, -1), colors.beige), - ('FONTNAME', (0, 1), (-1, -1), 'SimSun'), # 更改表格中的字体为 "SimSun" - ('GRID', (0, 0), (-1, -1), 1, colors.black) - ]) - pdf_table = Table(table.values.tolist()) - pdf_table.setStyle(table_style) - story.append(pdf_table) - # Add a page break after each page except the last one - if page != book.pages[-1]: - story.append(PageBreak()) - - # Save the translated book as a new PDF file - doc.build(story) - LOG.info(f"翻译完成: {output_file_path}") - - def _save_translated_book_markdown(self, book: Book, output_file_path: str = None): - if output_file_path is None: - output_file_path = book.pdf_file_path.replace('.pdf', f'_translated.md') - - LOG.info(f"pdf_file_path: {book.pdf_file_path}") - LOG.info(f"开始翻译: {output_file_path}") - with open(output_file_path, 'w', encoding='utf-8') as output_file: - # Iterate over the pages and contents - for page in book.pages: - for content in page.contents: - if content.status: - if content.content_type == ContentType.TEXT: - # Add translated text to the Markdown file - text = content.translation - output_file.write(text + '\n\n') - - elif content.content_type == ContentType.TABLE: - # Add table to the Markdown file - table = content.translation - header = '| ' + ' | '.join(str(column) for column in table.columns) + ' |' + '\n' - separator = '| ' + ' | '.join(['---'] * len(table.columns)) + ' |' + '\n' - # body = '\n'.join(['| ' + ' | '.join(row) + ' |' for row in table.values.tolist()]) + '\n\n' - body = '\n'.join(['| ' + ' | '.join(str(cell) for cell in row) + ' |' for row in table.values.tolist()]) + '\n\n' - output_file.write(header + separator + body) - - # Add a page break (horizontal rule) after each page except the last one - if page != book.pages[-1]: - output_file.write('---\n\n') - - LOG.info(f"翻译完成: {output_file_path}") \ No newline at end of file +import os +from reportlab.lib import colors, pagesizes, units +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.platypus import ( + SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak +) + +from book import Book, ContentType +from utils import LOG + +class Writer: + def __init__(self): + pass + + def save_translated_book(self, book: Book, ouput_file_format: str): + LOG.debug(ouput_file_format) + + if ouput_file_format.lower() == "pdf": + output_file_path = self._save_translated_book_pdf(book) + elif ouput_file_format.lower() == "markdown": + output_file_path = self._save_translated_book_markdown(book) + else: + LOG.error(f"不支持文件类型: {ouput_file_format}") + return "" + + LOG.info(f"翻译完成,文件保存至: {output_file_path}") + + return output_file_path + + + def _save_translated_book_pdf(self, book: Book, output_file_path: str = None): + + output_file_path = book.pdf_file_path.replace('.pdf', f'_translated.pdf') + + LOG.info(f"开始导出: {output_file_path}") + + # Register Chinese font + font_path = "../fonts/simsun.ttc" # 请将此路径替换为您的字体文件路径 + pdfmetrics.registerFont(TTFont("SimSun", font_path)) + + # Create a new ParagraphStyle with the SimSun font + simsun_style = ParagraphStyle('SimSun', fontName='SimSun', fontSize=12, leading=14) + + # Create a PDF document + doc = SimpleDocTemplate(output_file_path, pagesize=pagesizes.letter) + styles = getSampleStyleSheet() + story = [] + + # Iterate over the pages and contents + for page in book.pages: + for content in page.contents: + if content.status: + if content.content_type == ContentType.TEXT: + # Add translated text to the PDF + text = content.translation + para = Paragraph(text, simsun_style) + story.append(para) + + elif content.content_type == ContentType.TABLE: + # Add table to the PDF + table = content.translation + table_style = TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), colors.grey), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), + ('ALIGN', (0, 0), (-1, -1), 'CENTER'), + ('FONTNAME', (0, 0), (-1, 0), 'SimSun'), # 更改表头字体为 "SimSun" + ('FONTSIZE', (0, 0), (-1, 0), 14), + ('BOTTOMPADDING', (0, 0), (-1, 0), 12), + ('BACKGROUND', (0, 1), (-1, -1), colors.beige), + ('FONTNAME', (0, 1), (-1, -1), 'SimSun'), # 更改表格中的字体为 "SimSun" + ('GRID', (0, 0), (-1, -1), 1, colors.black) + ]) + pdf_table = Table(table.values.tolist()) + pdf_table.setStyle(table_style) + story.append(pdf_table) + # Add a page break after each page except the last one + if page != book.pages[-1]: + story.append(PageBreak()) + + # Save the translated book as a new PDF file + doc.build(story) + return output_file_path + + + def _save_translated_book_markdown(self, book: Book, output_file_path: str = None): + output_file_path = book.pdf_file_path.replace('.pdf', f'_translated.md') + + LOG.info(f"开始导出: {output_file_path}") + with open(output_file_path, 'w', encoding='utf-8') as output_file: + # Iterate over the pages and contents + for page in book.pages: + for content in page.contents: + if content.status: + if content.content_type == ContentType.TEXT: + # Add translated text to the Markdown file + text = content.translation + output_file.write(text + '\n\n') + + elif content.content_type == ContentType.TABLE: + # Add table to the Markdown file + table = content.translation + header = '| ' + ' | '.join(str(column) for column in table.columns) + ' |' + '\n' + separator = '| ' + ' | '.join(['---'] * len(table.columns)) + ' |' + '\n' + # body = '\n'.join(['| ' + ' | '.join(row) + ' |' for row in table.values.tolist()]) + '\n\n' + body = '\n'.join(['| ' + ' | '.join(str(cell) for cell in row) + ' |' for row in table.values.tolist()]) + '\n\n' + output_file.write(header + separator + body) + + # Add a page break (horizontal rule) after each page except the last one + if page != book.pages[-1]: + output_file.write('---\n\n') + + return output_file_path \ No newline at end of file diff --git a/openai-translator/ai_translator/utils/__init__.py b/openai-translator/ai_translator/utils/__init__.py index 33e52726..f476cbed 100644 --- a/openai-translator/ai_translator/utils/__init__.py +++ b/openai-translator/ai_translator/utils/__init__.py @@ -1,3 +1,2 @@ -from .argument_parser import ArgumentParser -from .config_loader import ConfigLoader +from .argument_parser import ArgumentParser from .logger import LOG \ No newline at end of file diff --git a/openai-translator/ai_translator/utils/__pycache__/__init__.cpython-311.pyc b/openai-translator/ai_translator/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e68cf9e43c6f640633209ae7e0730b7d4e7c80f GIT binary patch literal 320 zcmZ3^%ge<81l7yFrn&&>#~=<2FhLog<$#Ro3@HpLj5!Rsj8Tk?AU0DDQ!aB9Gmy<3 z%%I8gk`bs#lkpaxV^Ml(ZfaghKw?pGY7tO`*~j1APm|>qe I&j){-Y%*!l^kJl@x{Ka9Do1apelWJE4 h@;k`g#ri ugWW^=rKqZcXd0yr2B|p&WNEeWtj&gH*O{3W zVj&5Ya^S$7TZMXXRe^fTF;f2oC$bc@QcqPY_2!7=l1uyEEW6mDhlpA4^PBhH{NBgR zy!~Z#bcDdzXuWp+%@Oh^PSRl c%)Q+X tZ-lFkFBfbs99~OTBETTL z5F M$d#X)`w9%6UMndjPcVLjKKb%uSBymRC3!F zo-N(Lr!ojUDwl0ZMc8ZxT#8<`psr7Cf1P@^-w5r7Lz}kC%hOt%XO5_Gw}~LpXsJ{S z{JPt)>aOQR`D9XFw^zOBlUbz+dEyTZ=$b2+>1x0n55O6nS-44sBc E%hNg^f^)`)f#71- zZ-z37Z(`w_Z>b7I23fXFIl53P1))sMta`v#Z5hqp1{caY5h$<66?7QA8w4w5S_#(D z0zwuP9bjg2HeX@EnjejqN+Jk(&9Tx-MpqCwou#P++- stw@Xiw6@vF4&A&pi*rj&I!q3zYXDdb%A%D zzP5p0zgqqKryu|N?dKZ{x2r+Z@ol$sKXhv=0z#2h%&WF+yMC%m&s@N(wrlmwLg{*< zn!2-+7|PA{*t9Iycco=T7iao 1~sO>G1bDm;wbU&|c%oC$;vq8kjvZS;u5;lbzDu zQQF0hx!5)rd$jGTFP_!_@upTu{md&r^UBYBub=kRr%$i6uSW`MqhoHg&5b@NrGB8) z4?E_=cKSPl>`7k}$M&n5_?N)!Vk<=xHTJTWol5Y(1@C?)c%W_KM@Fl_{mFwg9#K1j z=RtsI3`GV`1UjOW7g18ojMSkK(d1iO_LhDW776@TZIQ#`dpjF1ikF@jFFh+>?i4R? z8z9=w^KnpD@RVBW%3D@EVp-4vLeBxdU|IJ=+rwsHS^ON>^7n!KO4K?P702_T2-ZV< z24pLHy86(-Bdtq%=y`qMLYJH-2_k|)0q|>{0DpiS2C1CAF~3S;` 8d-@)mSoDi$~M^Af!!J_Z7eHWiWylAG>FBdw?Skl6hs}85i7D4MBQc{ zBWASQI)^~A=V>W+HhWHyjGZbOJ4I4GRZ@Lg(&Vo~o~|OT_b;ZPaa76|b2*F5t;aoL zIBwqX?4mOY#C37(jq$Ou#ZJRv_72Yg^#QU_mNOCLTr%-J4stGnWtmE|fl1oaVUc1m z=}s<&6=YUL*id0~cq^!;$7sjnB)40k&yW>-%Y66E+!wrd-4I$~R!^1V&@;Sr#uX}~ zGVgU%wYz`rV6P+RD{`pR-6I?-3xqd0hco#*VwZ*zYIx6w7)0LoR~C1LZjTUqr^mK_ z8GEc8$HU}y@iyFlr=0`P>*%90(xtX6f_jN_RD6Hp-D^`6r{^Z;p(Vt;b;HVQJXJ{1 z3;KbumepdEFS-=38QwCLtXsD0xl}0_tCndKpsy5dhbpu4Q~G3jc7ibfR3cWPc*CNC zVVYEMZ@W}-J+oNysQS@-=ABG>eooJ%KbX)Yid`$eM6qKOEGk2@=wOwKruAtlM^$!S zvh$YCoCndk>=wyj9DM@l#Q4IWKYae=*Po_7%q$ewEXS}%u9xh^Ro63!w~#lS+~Trf zJ0UOZ%_3PaY`x=D^6b3301n|*jIQ0Limuy^?dke@e6~dl2@!DXehr|4_T$ghZhm$5 z%exPA_4rUDKGZ~9 I-ouE&y%SkjLr_Y(u(%Rk1xi*0@UXt|!a)JR;asz(xv z#B1vPcr9LyHzhQ9!S8!*Q{I%fma5{u(p!su`{p;}Ti1MLxULL0lwm-gF!JV=M^}E6 zePz0?OgEHiK=#iJR&lMT3iA-cLUWoxSegjy&PffkyRiJ~I*sajp=g#MqdJZ0`t_2L zZ*j=WAo~j<=b7F)2G}zV0P8q61)$QNO {N+ld6f`Aa zuZX yY8NCS`fc!ZGxMXio6Ht 0dNy!;Xm`Iwp;)J literal 0 HcmV?d00001 diff --git a/openai-translator/ai_translator/utils/argument_parser.py b/openai-translator/ai_translator/utils/argument_parser.py index 95681dc1..0253ea26 100644 --- a/openai-translator/ai_translator/utils/argument_parser.py +++ b/openai-translator/ai_translator/utils/argument_parser.py @@ -1,19 +1,15 @@ -import argparse - -class ArgumentParser: - def __init__(self): - self.parser = argparse.ArgumentParser(description='Translate English PDF book to Chinese.') - self.parser.add_argument('--config', type=str, default='config.yaml', help='Configuration file with model and API settings.') - self.parser.add_argument('--model_type', type=str, required=True, choices=['GLMModel', 'OpenAIModel'], help='The type of translation model to use. Choose between "GLMModel" and "OpenAIModel".') - self.parser.add_argument('--glm_model_url', type=str, help='The URL of the ChatGLM model URL.') - self.parser.add_argument('--timeout', type=int, help='Timeout for the API request in seconds.') - self.parser.add_argument('--openai_model', type=str, help='The model name of OpenAI Model. Required if model_type is "OpenAIModel".') - self.parser.add_argument('--openai_api_key', type=str, help='The API key for OpenAIModel. Required if model_type is "OpenAIModel".') - self.parser.add_argument('--book', type=str, help='PDF file to translate.') - self.parser.add_argument('--file_format', type=str, help='The file format of translated book. Now supporting PDF and Markdown') - - def parse_arguments(self): - args = self.parser.parse_args() - if args.model_type == 'OpenAIModel' and not args.openai_model and not args.openai_api_key: - self.parser.error("--openai_model and --openai_api_key is required when using OpenAIModel") - return args +import argparse + +class ArgumentParser: + def __init__(self): + self.parser = argparse.ArgumentParser(description='A translation tool that supports translations in any language pair.') + self.parser.add_argument('--config_file', type=str, default='config.yaml', help='Configuration file with model and API settings.') + self.parser.add_argument('--model_name', type=str, help='Name of the Large Language Model.') + self.parser.add_argument('--input_file', type=str, help='PDF file to translate.') + self.parser.add_argument('--output_file_format', type=str, help='The file format of translated book. Now supporting PDF and Markdown') + self.parser.add_argument('--source_language', type=str, help='The language of the original book to be translated.') + self.parser.add_argument('--target_language', type=str, help='The target language for translating the original book.') + + def parse_arguments(self): + args = self.parser.parse_args() + return args diff --git a/openai-translator/ai_translator/utils/logger.py b/openai-translator/ai_translator/utils/logger.py index a252b50e..126f5403 100644 --- a/openai-translator/ai_translator/utils/logger.py +++ b/openai-translator/ai_translator/utils/logger.py @@ -1,32 +1,32 @@ -from loguru import logger -import os -import sys - -LOG_FILE = "translation.log" -ROTATION_TIME = "02:00" - -class Logger: - def __init__(self, name="translation", log_dir="logs", debug=False): - if not os.path.exists(log_dir): - os.makedirs(log_dir) - log_file_path = os.path.join(log_dir, LOG_FILE) - - # Remove default loguru handler - logger.remove() - - # Add console handler with a specific log level - level = "DEBUG" if debug else "INFO" - logger.add(sys.stdout, level=level) - # Add file handler with a specific log level and timed rotation - logger.add(log_file_path, rotation=ROTATION_TIME, level="DEBUG") - self.logger = logger - -LOG = Logger(debug=True).logger - -if __name__ == "__main__": - log = Logger().logger - - log.debug("This is a debug message.") - log.info("This is an info message.") - log.warning("This is a warning message.") - log.error("This is an error message.") +from loguru import logger +import os +import sys + +LOG_FILE = "translation.log" +ROTATION_TIME = "02:00" + +class Logger: + def __init__(self, name="translation", log_dir="logs", debug=False): + if not os.path.exists(log_dir): + os.makedirs(log_dir) + log_file_path = os.path.join(log_dir, LOG_FILE) + + # Remove default loguru handler + logger.remove() + + # Add console handler with a specific log level + level = "DEBUG" if debug else "INFO" + logger.add(sys.stdout, level=level) + # Add file handler with a specific log level and timed rotation + logger.add(log_file_path, rotation=ROTATION_TIME, level="DEBUG") + self.logger = logger + +LOG = Logger(debug=True).logger + +if __name__ == "__main__": + log = Logger().logger + + log.debug("This is a debug message.") + log.info("This is an info message.") + log.warning("This is a warning message.") + log.error("This is an error message.") diff --git a/openai-translator/config.yaml b/openai-translator/config.yaml index 2b8bc837..26d09952 100644 --- a/openai-translator/config.yaml +++ b/openai-translator/config.yaml @@ -1,11 +1,5 @@ -OpenAIModel: - model: "gpt-3.5-turbo" - api_key: "your_openai_api_key" - -GLMModel: - model_url: "your_chatglm_model_url" - timeout: 300 - -common: - book: "tests/test.pdf" - file_format: "markdown" \ No newline at end of file +model_name: "glm-4" +input_file: "tests/test.pdf" +output_file_format: "markdown" +source_language: "English" +target_language: "Chinese" \ No newline at end of file diff --git a/openai-translator/flask_temps/test.pdf b/openai-translator/flask_temps/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dc3e982893555eeb94a21eaf400d2e716e2504fe GIT binary patch literal 1560738 zcmeFZV|XRqwgwv8R(EXMNk<*4V{^r})j`L$?WAL_*ha^;)v<5-+j~D}?{mKAp8Mne zJb$VxbJUu1V2)YuJ8D+Q6-2+%Gc&Nlk@xNW-a9G2&Y9{PfMX+KBC<8KfaBvMV*F}u z 1*lzCZfJY;Q__Aa#%qgI*s@jHpamIj^ zBhbtHfB^c|r-48RmrK1VU$VIbkk+3Q-V6yWkWBckmN3N@@mS6^q>dughUYuqJ89z) zlA3+x%Pd+WACS_AQp{Xnzg 3P@FLR0|5X*2B09u{R$RuZ5&2vR*QWw1%WRQTCCnH^o6iYC~}QTCF)b@%(^j& zOdo{G_mNa1_97k9y8dpcqWKapToo$27F9XXn_I*~)Z{$f9S00y(`A`&2J;3cbGbc& zMDfmM(&nk57P1w~xy`0zY%qi+B0w+^ndxixPb}<360?SKT8KmSAaG0SGHrP5KWh@Z z3cY2;>(e5C1M-@V)S3~P&(f49WSAjYDqBKN3sQ=fsF^i>;8(9HosC~5>=v{18uuQT zkEEIdEUEo*Nf4{*CBhbK5{G;iMi9)Tsl8KG8xzoyatk)4rOCWgrFa0; pC$9%S4rlzl4 zaEPhtn2M)VQ0o1So3+x48MC$~#YL7zFF3`;QAO26RBO!qrZELBnYd_8M@2$Ormc;p z4!W-$IjtyL%qMMX3~jblrs2cQv8K!EZ0RJj;oR1M@Y0ybekU5GCzigCqo(YZ2MmzD zs0Z-lNVy2XJnZAh=J`ctcC13ir#i0@#97q$A DD`R=R &?8pufmIDAGt4EY)8J|W7e!cK@zmw^4EF_mRBvCxcRcHpBkIFd z#%VuU{i5f}?%epx(%mnA41f*M`pW(YluR>M7~ROatE`Ww4%_&ewt@0YOLtbD#2VGs zn0e9&2ZQHw_*#t{$q0qfcwxi@D zcErjfNX!9m#r6YX=EB2CBkL}{@aSMsraR%2lhkT;tdB9NnZR%$1WCph`=cB=! zPFJL(vy};f4V%$KfL#v39!_@oTmkQ+TPz{sn0ShV&wPBn&+SmRd<1~Y1zIse sd z`VRY>97$Jk`gZ~+9MBqyWU+8D*l;A>{ZDe7hVz%}SBkp)Do%Rdgtg2WDLigiIpLDB zeO^e7l!h0vTWh^gwf>xF5+NZWh&7&eJdFT&OXECx9`!ku =<&OZ^2K%bq?J5+3}QgT@}~vJ-Jg@`@+VJ?0Suk%TljNn zj#p)^ONnP#8ZXP;#I{;wdjvL3dlvAtFYhN{QS>ZJuzA npcxBi{U9-%hvXEG}NZ%)6Y3 r`J{NXyW& H z= %en{bBKcfFHk~5g7 >1p7n-YF4G|O4RTEu??es}iX5!rA;TBJ$ys!o^l4|e@Y+lnNpVEZ)rBZ23-<3fT< z@e^4OV*yJ0r#yM}PsBcx{VCu~uD{m|()Emz$9c7lqsAMmRD+P!z>(?o%{S{04b1g( zF%p*uITq7a93PZdbr!x8)6G9XG2&EwmSc8FgXgw09Nr@D^QthI+$*R#gq7?SpF%R% z(YeqhTvv{%UI1)-{b>|d!F52LClOzPv1s!=W|3R*lUL0(fT)EtxAXqi$GAwaWstTe zmp}64i8*~(bU+MNjuIDCdqb%fds)2q)+!`}?e`=0ObJ}<5B!Ggn}^>xajPqA?MxGw zX)59|p7Y1ey-`}%4AU;-_H*4&KUUz*(-sw@R335PHLb5H^b8iLR;v1JheOwiZ|`m= z${Hhm7VD~92t7}@J_|e}LX05>7BySnxVGgZ7G$G7;X2iScl+HceMS`Ddr0bv;%|!p z=ZXS7=LYS5Pk44YCI| e?ZU24YB}Za|2;pHzF;jj|O&bb|Nk|79BXoZ?;Y! z0O+IX1MmEE#6La!3nnQz*cvGToQSkOh86uv#Hb8#b0X3qVid8pvUO0hGcW=W{XtVA zj?6?He+?uc@PVm5#{R2WS(%xL^G`Dzqp~s!5!YXLd|a{;asTU*jfnY=LLV9`D{~Mr z|FQPlhc5r74)Y%q|D})Ihrx_Of1sPqAGH}p0WRi7fN$c${{`MSK05hZc=zv!E;Gqh zS!oF?WD>{;oI)cZBEP}&fF+vYmB55h43+d#01FPJa1n+4ih)Y4YOLG^Djq^&B!&V1 zJ-~1Q_BV>cK)0f(*xp)%CG15d`}KO->!8NQ(m~V0lIex_LMw>EO8`WVlRBs$Tdq3B z`AN4n8v1D8Js1uOD3Us8*OrM1g!qFf*sm8Le`;!p#6aEMIfI{Z+X`K$0)^xJyKgKB zrwADo2%Oj)2cHiG_8wS*Qe8hT%R9u#O(Hc*Ff$8QE^J-}$xdIj)m`yEZQF24GR1B# zp2h@jJ6l#no5Fe;`Br`eM6PxU`KD>U7c>B {%tj*_p=tzS(2v(#B^WrC0tTk7t!Diqpv~p@=Mv z4Xe`%Mj#f*9hqkOr@D?r6_9#ss4sh474jEEAip6&>en-_KL=as#`SF@P{N0wo?1DA zW?M4Q9&$cs2iT5u26Nx1j-EfO@)ltW`jbjxvI4~Rii(S*MaRYvkdh~jU++=gce=RE z$A^3AGP1j{uYwL$6b}<%14GT)g82N^V;GF#0)6MUd~rA^p>cgOEiufT^+3FXxD}cu zp@(ewK}1Nw$!r8Yz`ReiP?+I{I@--DW2+sbSv=kjo(SQ!!)&%>gh-V3?+~Jm9mw U?@u| |dqy`!-7qttvAb5bq?)k=keYh1x9~4P=z{cm;$glF!GKSHRfoa9 z5*3b7Cj&(#wu~hy04o(si$N;DZ;6zMwciDY3duD{!ND59-3if%*fLZ#m^YMSnoBWC zQI)~o=P*HG^?wW~G@Pp`Qm3pCbb;kSO^>ASQ`L{w*ZP@U1HNc*%1eMW*oC`IVCU0} zqs?N&UIo#Fyb#gmchZZvMRDcUPP2}25rEw7cxmWG+yUoH;QM(ansA_#G6q%?!ZHAv zIEjeNicA>=59R>oH4wFjP0_zZ-iag_#l274kh&&nQK(fI==FLo%2Q`x_Z`YQ4X* zqy>o%1wRFlIDVMNSlj_eeu9A0#Q3mX~Pw!!XS-)bRT@d%q%idz^n<6PkSKm!qFEo?rO zi1>)Mi0Es!{g%m{$rX-jbKUoL>cmGH8yc@fl0=h4(8TC685P?(jJeD*>oWOYOcoRt z1{O;e&vOjrk(0%fu33|rdTj5zFCoW2TSGmXpDb>BkNJ<|@zU@*@Nn@|@RHdE*m2Wr z(uULa(hk@L>&-Ob8z40IG!GhdnG#da$CORT>%zHnE33|m>8Aw>*2?)SjY{g&P4ngI zCeU@_7_>NrnzWmgT!rPO8Pyr}sueGC1r)O@vkN-KJGFdAx~7Ph^}~hIyGwGzayt#O z4Dz>(NXuf&r)lyv^LYojj2i93t^{_pD1L@!O2$b>4$CSn{#sYoZ=I~4M6ld9qhKp% zZyU_}xvHU2@pV3jyO(dsE8ji+krtjmTs=G!YYj_}?u<@}{)CQDgIWVp-Maqx=Tq-; zUz(+kk@sM*#g0+kMA6!QLuD!bhD+y}H*cEG&o!(zxwgqWq&t#lif3kM{t&kiAkzJ! zlM7W_p!Xc=$>CP>#8t!&G^HnH&8%ecMyAFD^#tNXK0E^UCH60R9EWN9t!4s;SO#N; z2t7Bu++Qjyi94DnPh+wRkq0h))_rS>{7bLR&$P(W$g$$#g9*j)2Y8e4x<8Vu)ne7E zo8s;bFDx#K1$YEP1tJB|J6t--y%`@2Ucp{np4P4xE)Sj-p5-C9ASz%F;R+zWLa0Iu zz_Y-jK{`Y9c7t}e1rRdM=#gpv!U7Y15pfCPgJnn1!F+PWVW4BHpg47JR&n!jbA_Nc z6cZKcYm yp<%Cdbh0W&uUo^O<}*Iwrwx?%Q1qVCJ(;Q-y%yI! zh=usJkmtCA0osGeE1d_xODsfkU~S;EXmX!UsF7rqB&OsJn7mNKrgN>{Z~xuWgqVbU zYSz-}(sA>^`iJ^iVRD18pR_ux{FFe(UbcKDk=7COxpNityf4$SGE|NlCT%HEZ!)(M z5oGD)-! $ zv{zc>Ty?rzw{AO5qOFOnC6+Enzm7%%@HcG2)_*DHDE{owH> `bUK_kKYp#?r;5VZRa-78pD3R(9XtZr5GPM+i)w|1KP>iLSENnFKF ziU)}o4)#Pl&+-0R;4I-x;OL2r8>rc_AGCelycO|8FF#UX&2M>t;3DR}+8%r$yvRb} zTXGNcE<2)p&Aw}#(hSqgonM^~Z7kq0_muJ6IP#m#{50{2UNHUL>n7qZHLY&MRC+RY z0y86>JKNRgx9~vFRd^_N2#=bRKx_T(OlH;3s`XL- oIQNx8b@W`(2HEE|Zssqyd;7RA1as95 gk-J z!}DEtmH)_l{dF3;6j@hr%!BLI?A7RWCyx?No+hhIu<~{8VZo#ncypSwpg&pn!!$a$ zS%mRD@v&OI7jm+6lJ;x;*JJTY@yUqCh&7LzoBqquh~(Lwpib(y#{0zS@@ya1XP~L! zaNnin^hv6q)Otw=_tX5H^?~u>?yRf%MNx6sdU=Q7o&Rg@v(e39?9r+GgM4Ckq@b%8 z?8|RA3Tu=7jr+js=l_Y!{{Y@Uxby=XONxjH8#n@tiT(ns--vYng{7JQ9Zdhf)f}1G zS^lHs7!(ae>S00%ycg)_7kaazyhVV=&rDJLB_)Aj27e(xlN$hex|h`UY$F^3Th(5Y zv**_`2+O&+wjGPjh2Ab7tE93^4OuFc(HK>;D{rgR-iufA^#nF$*z6KQ5-iS`E%xQ& z$GF m)$_zCkrc)|yR_$NX6%SHdjP~iT9p?pxA|H)AP>f>LO;J;H8N$U@O^uY)I zI*a`CEcw5X7C1(Ac|!|;(LZ>Lq%|`U^Iyc}zfqAtZT}kgAIARcXN*eDhE9Lcv%khA zVpKCX{wO6gvv6?{F-ic;P0gH$xH;KB)cG*m!Or$i*_p`6!5IMe?_7%cpA_lOO&=VJ zQ33G5>Fk`G4XlV5h0UEDKmHc6wYIah`6K=4_@4#qkIBQr&dmh(ztWylAnZeCzw^dL z(P*c6I)`K(m)tDd_;JNV2V+!bfd-x}9atb)U| 4U9PcLjK6e9E%6%=VlfndI>mgOM 26kXed}6#t_Lvs?qRJbp{+y|GL6Br2Y;oO<$vq+x`e?v zciTDls0F;akV(aJiblhD+QgC-W*=H{OIDx6&b7q<9vaIm -Dq)wl9OHf2 zC`28H%!t}vnDv>fg{N^9&@$Vppb-;F5IVDqC`9e4XHUI$6rIKO%`l&N-SMQMQ_@3+ z6#u)#erzi5N@6JFy4TJ`=GGNXiJBu{8hr~*zEzLV^bFf|nKL{~d{Jt=u2oRs`)$pM zz)hF`95iE;?69O(-Px!*Bl*oXkHBw^n{BKFl1L&y#N# 3x?>nvns9Y&w-sD=uc zCl3|Fx2Mt*hIS95oA@h~hkMTjEvpTm`;Mza{SI;` & zgqsWB6;MmEc-e}UV^Fu40GkeHQ+~jabvT{cNSoKTcpjU(2*^Hrbw|YKfXbo3E)N^v z9j9b ;%M)? zGJClNmqa_=+KU2#4h1Y0`r1{N3npb>4k{J0J2~0S7M(9=)k={?$!?&rrSfzg_QlW> z%G3+x+`znNJU~`&Qc *!@*U+gGZ7f|^q047Jl@aoeLnkSfIjJssDb#*2*()y>=BDSEax zDX#QrtEBC>IY@XXk`A#XOCclk;#5Q_Z^_fF706$2SOvXwL@c+V{ha3kL`#X09_(%3 zEaf*AsIJKy*~Z%~&RWg{O|EVXA2+x8!R6Cm7dY#86#rn&nG<0~8zJf$JE() uvXi8fgf-inJq>3d-w;@M!JKy@MhxmsQ7u&t#_SZ|MyOd5ay_ z>)z0M-nc#mp`G`?bl-iw?u9--USXo}On%sG*eA;tGU)zN>Z(I7X4FD&+i5+DH$xdp zPnq4iO(*mGP+?^LULLJ`J>;Fd(~rjro<6{0zOw`&_>xvh=yM4-0Kc?EF8JIfro2={ zyh<`EnZ&5!`q?=oWW((gMP91}*H%SbXc05&8(9&jP?w-FLGKo2&Dn T4=?PG{S5mDP3sN!rW@WU z(XrWX2Z=XdlDz1t@jr9d#4K_!M7r&a^KwacBMi`3ZtqQgv%wAJ;5S$iu``8GaFS>z zxx4}p`S$M5dK8V12BFF%a>cythSoejX}M;(?m B=kT*Od-`vRvEK)b77htYKn0qA8+`#q%17>E zzLz2^IVC0B@VCjK)25A{38L@K#nxhz+L{WN9ujRA+kBzrG^XT)*biE^tmAWq^W6)+ zu4~(>fU>*&l7ykBvP*~zPlGJrTdP4bLwYG4O&~b(TJtd})u5|3NMgdm(hAhLLoxx@ zj6B46NwzSC3Nb+en2n(Zh?MD{8Bf|}(P)uxAQThg+J6*PB$=@Qz8S&j*Nqe3kda3J zKp&1Rw{nq|>Y c@&SPfS8s< L}awc*?bt%AG9HKUU@Wf%o^QALDwHZuzXb z<%}Aa;+BH@EDp6 dAu&LE+%_EFTb#kMR<7DvBUd6RW=u-lw zbHk$;nq{!1102@2-Ld&Hl1oC<>)Xq0pRo)zEo7_pnOV(MJtlm8k6S?(!S9Q)H{3l> zUSTTGeDCMs)k_{jU3L14LZes+yQ- B)`4;unr^jm7{^_ht50(~${!e^n-(Y864= Js>@nWH%0aM7mLwz6Cz1^Vqpb#VG_Tt#f^qa;o zXhBn74{3Fsag%Lc&P(x^QV{7_t#V1og){bZ7^p}ObZh)0ji{qochc0Ud% b0Q5G z`Ht9+=nQoUib&ekZ8A_=$glnnGC6dOlRpDdEq-qZpjk8Ygdq&zD-5u~!y~%*q;?cI zk)X3@%!w7jY`^~=&R5Q^ao^v=X7(N?fN#=PhTcoEXlyF|&0PyA9>qcqfEM!%f3`Oz zwk~aGBprpZ8}* @jUjWR^%m)VniI!~@M<>mVO{ z`(4fF;pLdrmL(Ru0CD)-Dry_udeUtj$RjJWIjdHM@{Ot!uX;{AONepyVUp{@N@kEc z8Oe6AgDTo8jdFcPJpsk7^L8da9ZV|KP3qSf4RxMgLe5dFG8A69D)-}}?g KkWq`sQ#LgAWC#r0vf!^)8+qHqi!XK2+Ln8qxJrkeR=3MH zf~}sJi*2=KMOU|fg>PvlAVC)wASDGYR*ZGe>Zhs-o~T*pMg{zl2pwmRWu &!q)Ydsh9OWknds=9+ewKhJ>O3}hw)Dt41PEV9mXwOHcFiVSACJ|de%8lNSS8z zPBiWlXhocWE-aV?w(spPduja&YkCUPQ^6yOG;0NXd37;`M$33LK2 !!1UC#x@7_!DHFaY5pz_XV8*A-7q4 zd7EFFK%AxTbVCkVcwyfal!i5GwhsYuS)b4)E1)TBekZ0FxA;XYYU&S8XfcFh@krtB zJnkyETu>?IeI%V(DISItnrE@*0{358mh$n~yd$2%Bsju D3Sq!a(q(@5aoGf2ynv)Dax!4$-kSH!i}rIWmwO+r zRRMbqgy@8jg5>sI4at_D%aZPGiDtmJBv7bj71qtG+ z;6zorzY=Qou7*sqA;@7oKAvnzWkR`RHLhjOndq1giJXxdCBZg?U^?BBX}b1iR{3PS z<;?2c(}Mj8c+jC~Aj#S{0~ mY)a!U(GtCR3xPPq(4E%yPPf3u|2#yDJk8lKUoyV)+Bj#3P7 zA??y^&Fm&?PuOR^(1z)6Tx*0@ ~D5LsQUge?eNk&kuc%1qm< zBioAMqSb3S>Hi>Ip%KX`RxOgok~ku+175Mc-py6tEKN&ID1qvM*6nFzdn5VBEoIo{ zETbM^B(3kT$gSfA>NS*c7~Mkpk`)4`fw*JK{2GSF-aEcNFS{(U->M!a&fv+p?h$hl z>Xi}U$U}ijl_wb`UDfU8W5VoC&nSm*ax;S%Ap;z7r-wc24+?A+IZld?O&-j!H<(bL zQ<#~+H|?1Xx&&1zu+XGAd*ymf6X2Jw$cU<2qxu4KNb*Bg<00}&k7~D` 3_DfEm` zbn44HYk1z13o#gc5%zc3VCW^CoztQhVg2TMjNPg5d?iRQAW;sKCXF)6W>TQ;4@NLl zq{!tmR8`5MApV3*GG5kB=iU`z iaJBid0J>eJLdnY7+UD^5#6ArJ9i3E=6XE;HdRhQV~JwFv+}F(_0%Wkv?>cr_EU zuJtOEF+=j|cHIh_4m6A5{O0>og TH+8Etc4Ohq;3}LvG-riH@!7H)K{bf_T0(OEC`x@@XoZ3bn4zv-Bo8YnoqD) zEO!`nvLb-eWKCL05xei{vPQiQo0|MPDrHNhvWfE5V a%e*m!&?&Jm;T}Z@At Pz{7JhMxUL=Q>YtzX0Gqm@~ z3XHGPBF)rR&1Av*PyE|SdH2is2T{ggjB8jdg$x&CfMjx+9d`8rOg$H(G6mMmgG~`G zI wSenc#PVE>ib4r3A!a|f~02$Hwr(d_K|48(VVBOWyKdw_quo(Xq zDpT-4ztG3g)qft>#Y0>VyHBVq0Zh4-*FnJo*&`gZpP&57>p>7xOyArUaLYW8rg7F? z4LLgTK;JDI2!2FgfOgQ!*Qg%%i`wXHf7s)s87)@#I1%MSjd36n90{Z)5gc8BkJ3&7 z^8W2X7C_Q-y+sDH4L2t;s32R`yTfUtJ8qiRXu=w_$E%&ZS)E18%5-2R2%j2b&8Wk| zQFOouKTtT<)SN4VQIHRYy=o{jG>wDVg=R9~CVFUyR-|+3Eh !g<5qKI z6~9%elLuy-C0&-)HokU|cD;0A()bPd?P(>whAN2Tc^;|gOi)X5F~wlceWaYdNzf=j z51TNaDU{RX=yOFp KJ6kt0SUSom*rEB-Z-=F0eI{YWw3`vF_ z;x{$6F-L&;5a5y`C`txhYLPeOz-FaNeD14udVP^51plf{zvx$Yf}2WP(kq?GThn7c zDfHwUL)kBWxZnrM#-@G=SZQRb5kwb VJ1dz aoh~s+xBd7*u9Z{JH$xi5u28?{7eQ{*i$lxeTAHmwyq8?2 zu1%B3;=D8t@Nqa;W39h%_?n{
Q%u$fGbL1G*$H <1 zpxZJ%&XSa@^QNhEg?QOBsg|BEqCyf3Yq7SaiL9WM@5DDUOeK1#>( zYcC@%5d5m5WWEZ!9BdR;Xp*Kp-zw#}RjG%JGi-(v1^mS$=X3sfyh^tx)2nh_>v`+MyU~~Y(z<}LhdTB6h< o;5~EaIi_@J_Q%ItP zi-ppjBc6wSF;+EOq_AQcyJvE3b`(WbD0^3`VbyT{N<9o;n&rH|*j#wGP=o=<)EmkX zKCz~~K6q2~Qafp{6{h`erKp~*N}bcMbZ=@ka^1Bs&43bzM7s84psu7nDSI;+(06y= z)xOU5L Y?8xro6y_0rLR^fCPcUXw(e~4pMyg$RggXWtPy`^h)){GG5NM z=cpYuq?*nAd-#Ptf`Mkbgi_-YHxn`TM1;hSg8NYB&ksB3AyX~KY+H}%3>x%k4=d~K zYPFOyWlm2G#mGGKOiLrNS+kOiMHu$qzvIYY)<>B&Z*!=cmFdcWd0C*sa@mz1krR~> z+N$u~%`MRBG8;S!s|sz{PYRI9$V&PlQggRd=NSe4W>b*D152VocqohgT>T7Y;vj8a ztSM3)6i;UxK09^x&&P-c0h!Ru*Bj1h?H5$d=^ZoPxo}kbqxTAbPZ1(qupzl5MEoil zI >k+wQ37V8>^Sg5+O%M={M|Ut^=y8S~c?r0TB{XRKKelV^ YhsfvHus9jLqV5r9EdodbUN~G{V zI_j^(eQ0EeG?;(}xUO(lsuuf&{(xROCcQl!y*_hP%_e +z#HCnTE9{Q X@Gb?r z1A%d<$et=~olm==&oFk^`inoD=?Hh#zkkdNe_K*ifmHdQGBK!qXy_hEKDZjzU-`*+ z>mu36Z7ypvi^9={DMS0cpdhP{lS`5)uzoxYVA?$1f8>8`iPHyZV_IQ)(QAAs5tv8d z#4z}D>!H?cjLM2&0j+>jjTRa(wzsp`3y+5rw)l-^&dOG4uTGJ%DuPA+1Fw~;6`Frk z$@I|RU`*VW(7qEsMTB*GkS7&3e%XSY4;PYTD2VYXd4H-I>KQ_yOO7>vVg|#BJ=Tn& zC%5EOZ{g|Qm+gV!_Gt?P&yr;E&aeR1V}LC?)7!XctWz`^2zU{IG_?%C)XCh_Cl}FP zH)0?rKm5`#i $cX3HDc_UIQTB+FA*?$}W!QZf#;M4QCNidZ_3-=7WQ38c(M9ID z+bPa#!BD%fJ%IG{=}eXZ-B?|IO8(sql6lToYl6y2%HwJb$_Gq&Cw~MJO9r7()a$pX zx{(lAO(^+T_j@dSR@QQD*p@m_KPXSJnv0O}Dqz$dLX!L2#1**0sVC42vRlrB`8Q=+ zk W_&M3ECESu^pTSSd$xh{UzhryIkl{xE zd2NUjS#qM7q;F1Zv3y(mV#?ke>%sgCUwdp*o^PNp4ltp`jpr^R!eXdhk%`OFXx;s6 zK$cDClSD-tQS&=nd#0FR8Dx;e3Arc9tXD^Iq-a6Oq4DO9X)YF1XEX)5PKhtB$gg)- zPik1Q_2>-ygy39yup&(Z<5~`+PX&+f7qWQF0BH_0SAYWbuVgU ;cEvzOS!^Dr;@ zw{_3oufI_Dm^S$>GO6<1aLRm5RZsaG+oU1+WhkM0;%kbm49;+N!uFNSOcxrO@OC0e zcw)@&uu!Zkkv4`t?2=ESMBmYwWWVGHr#1oecVjD<6w2^!Yk4s~KESO`(u}VSiifj> zpZmj0(7EE&-)XswS73M%pJh#{YFzwl*m{tI9e+5M#CZ7y3Vc<4_@TrftLW!3WUphZ zUHKxB!&Y-S&snuUe%#+~O%N(24u*vbC7`v%vx-8g>ppwer> H-wOVB8QVzh$E5i{o=G(xmF}Z?g_#^nDuWdML15CxsJ-Ep*ya2w zUJ<)`zyWeMmJDgRB$NSlZ$RqWv8&YytC9gQbxly%=Wh7}7SR5Rs9?rh_Pok_H~!>1 zo}-I3TmfZjU-yx*D>HJ;oBDHB0Gi _WIVMj~3$BCk_!PSA*3g^+uGo+a zqvYbnBtz*mm*k!Iv>7NS9I8YuYGA;(BrPoxgI0o1DW8mO$R7PaSuRNm*_+_z8orOY zh-At~^#j^C`*Y#*#SC!)iI$>t?I$~l%QoRLDad&XX`V}r19t*#>Y( +x7RNyhf_N)QI{&Z?ILC%#B?!VE@eU0VQQp8ED@erR^7) f4|7ad$qDF)Z0kEYR;=Y_ zx`-t>rNd-Q=^o~}OWQPfgt)hFqLtwgKV4{3m89%T(A=D-_PEkB6J @UGB>aBTnbO#^#Wbt@pw>Wd &8qjGI0qZc_e6W9x6k_|;g5{$___ma&h&(LJubt_XFH;H$Ac zTaqu5J10aK3G093K>t|Qf RZ)6Tv4b% zhay$D=+<%TF)`r~E@vHYj^kN*sv&p&GRH;_+W5=bo`bJNH=?1PoXVg*uz`ufoQY<0 zHQu;yF?}xO)FRk5K57Z}9-9-Z4{aRBOD+$4(r1%XSW#-g91>ZPI{4a>6j>?(F6`lp zz7@pJfX25RE|6!fXu|m&%wgQc8tKpoM{y!Nk8|0dTnK@>?OfK(2y$4ujPpU-w1zdQ zhFg2<5YMVTRKGM}mrP`OG?2x#@zcnZs!1CaoXW{Gj!^5=(-nINsBe!jaZU%apHHnw o6}cxMwauuTI_Hb}Y#Ck=heY z!Gk>j`pe}Vyn;0n@&|!a$RzLqU)XK}*!otNK6bdC%Q7)Reb_(obox5IR=gG+it9+y zP^&cXrn%7gh4(tE&2BYeO6cgiD(12?jf|H634LVo%A_UJ9vJxX3S4a_yi(fN8jLF8 zST}UgPnO!uFY U4|BG!lT>aIHlZ0Y1$9 2?(bR+4LExOZ6cNIm+4!WTv^gI_)$F z9>wrJe?OA02D&j;#^tsw-i1~MOqy62I)OiH!^YRy{g`S&%DRVsSL0;`M%1T|oFE<1 z+>+{9ZOVI?U3s16;1e I4HM@FmXBPLHvBw3O9Lzt0|w1++ ^SBwf0*ecR!m3udV+MV=$Y$*Yc zJ4=_Z+P~V$6^@Cs)+(;y@}3`sQ# xaS$^w#lWLIx0z_r5hvtpxXe` z8Q$n^607Xf?cV(7;t(;zTIepyh45@o#Q_0ad2PCu1g=!Ic2^K}<~nSO!P)Qw_RB$# zJMQvH_tQXZy^SR)W(~Ej!aN|neMV$5nz+}$Z`DR8{(0$fbz-}ABu3}SIFo22f_tx_ zbD^q)z#YFGzC)oQO9XoO93CO~vWY3l^8Ta2KsdWMed$xoN1%fL3e;#(-_7(wD>Ia- z#yUn_J#8|5rgSg<=m^D5g2oI_y#oiJ@rF(+jz8T8VWXIH|2*uLlfj AC&JLM)?#EiB?H#ewfl2aX?vrya^i%?{_Nnf%6@lKCcb zrX1L!<=}O+SoN#~ev0>;fQtU_TZ55d=-PE(=G*Q(9!GS|$rg~7*=#+pb!E~9pY0VG zu7*JOP7_7mQkVwCKK?s`)ojkad-6h|W%z+gbp=%N+(RvcBPCyXf5Xp4YFJ0H>Lw%T zb-L_zNzF!LsaP`}$s<0KPX{K_lX+wSz^vneT4?+?uX*O$g>b>kM(_@jx4XP=)B;(K z^I4l8MnP`LuP7zq9O_*kk9IhoKrRK5P8u^ZPwILj%@Z|qd0u!wzaNI?twV<>&HM=) zZ|)#Zs4IY?GWffmHV(6iy_BAX#`!y*#;Tgl5S^lf>-A7X?7xT4-yTKX4Y(*BI;1&| z{|)e=Riblb=hpt%9LG|4gllIz#RZ^Jt1`+~%TD)p#KKM^@&b9)2351OvIl3YqAK&& zCUfsPodMTZ8pB2}J$|=NP5 EQsOYXX!_ZXiXRW6#S!oU8)cL?5bZr{TRP>gjT)MWRaX~^53da-^; z9AWr1ctv=Rm*9=x`oi2Y9$)|V6X2A7 I*BAWfq4?dDjU{S7u-!hAoRuqb6YT_?b?MM; zUVjB@2zQ!#QZL!Al;0(Ac;@0Qv$Bz9B!%)Z5ks@0-y3nuG6Bj?eIsA$TsGPQ4 zAbp=7j&%fr`bnXUh8?}jGY+n&o~vq<#q0}?b$WcL)oqQict-zxv5WIkLL;n>X8bAt zSIx1yZ9SyEB?3`xy70j^mvKruf7*)=3}g!BDvOkI4QRr3@cJJQmFtx5RS}mcmjndq z)A}9Qvt^K}>H56tZ?>yK%+BOy8Fz5$5_3x~?hl_I{ 5)$etb(D!k6w3Xkx{^wYa>vl-y z58AZAxk8W<*M)Mxc=qjmc2AN-Lp(otDArZcAm802M6^Gzm?6=Vs22tHJV0a#S_+@k zRn+lqRL+L-0>tKLPrM~YZ94(i8~7<#Rd Q9f6j%o=pQqGYuxSX1I*Sf$&+~mjRX+WXduj%##T6I! zb!%l}*~U#rBQlzoMtZ|^@^PM+wSRox;qn%NM))3Q*Zqo;s(5l^0W`Ka&`G?%tH_f1 zGQ XuDYpNhrP-Mfb*1*5X^_cO~>xrEaUuO@3 z{ajue=kR?$ujHgIYcEv$9ANL-pNtFVQ&%iSaZF{@D|&x4AM3k#{#a3&*Vf#vIW`-2 z*S8yZWS_ci>~n(Y!LRoaTT9oU57jWYuX}XWsiCzm;NdNpa`2vlPTwvC@9eyl_Tbh1 zt &sWU!S41F6WTdhZ+*TWWMC zu0GX3PXo2zwMwUOC?ai(>7}OSBp>rPRqd#u&csAtUe$rCORcf>$yB4&?W(b{fDLmT z=n3_V`S&guF~?X=- CDOee4Hz1z;Trpp_%<2(_zbKT>6S>~sRpZ!q$@nhve zEqo`to_wcTZTHi>@}3*TVs~hZ>ieshe6M@*o@~eU@*e;O<$Rv`84EZRw3Y=#_{-o1 zfdx=5i6QE{NHUz26_aoF^%*CYi8*!{IUMgaYR91+X#uCIz<~{^S@;d*nsqf5z}-tm z*GR_4vm$a`NE5kXd$TUrWF_d7Fkf^a+MaeVpz}>yBeI%M1+k^A)kDs-9>>{$-!I*b zRTr8Z7C-0f?RcS#w$i9#G~>jf3{~T{J}NHZu4R`&%s=~zONQR3R&jE#-jk@tefU@5 z6RcZnj#IjM2^Va6LeYtm<6(LYGvF-`5Zup@ee>232J!6S6KN}v+5rn-cPQ=Ja}w>b zVH_UwjG@TIqcU+0$&I2)+outg(q@EG)~g)naF$PqTU*VmXuW#0(n?J_Ef_U};(~~S zF>7WO!cOH2mLG1+2op(&*+SMm*0@l|XYqQDKXneib4{CYdp>Ta^EHDDcO)C{PsS+b zEnDfxcS!SEs5IKS0*-A8jB1<4uzyO%X7~DT*P$oUAUJ;NQ1@|5F<@(wmgy5GYaA%2 zi>!1Ps|JCLwHK=Ymtj*`-U+`*)iL_swEu$E UALk=?J *_ICe)nu8QUG-C#eCfP zj-;cl3k*gj&}Lf;SUub!M&S-PGSZR%vz;iZkZ6F|N*^oB2rJ%Q6nZXf$vJRM$&e}3 z)Jn}xllPH9w9|*AMx-qWs|D^}&RRU%p3?!gFAW`U&)?}FOt-)ZQOe3{&5nA%F>W4A z(;XTo#95taa!{|>;K&2nD2XkmCcQf1E 2Dq&H5s_N`9PY%f+h+Sq&7PPf>IF%C=r3nnlBlSJzKzL2gTwIbdV zRCh*k9quY;BWoPqO%}WP+3@du`VOs@UF2eu5pHoPkdHzXe^Cb|TF?lS`K>bfDE?Y< z1hFVT(3DO}B@9Z$VqvWcU`O7OPz)rCQzWLr?nHj=@F-23OT-xnP&}9!ct%_W@2>() z;3HJ?GK?|_Fr8;mWG~GVi3{JZa!hhJy`~yqDju%6xiA^dB;HrmWe)DfE8-ocvM)BF zCgvKki>O|E3Gr~KJm@>e(@84P$A3r pgap;c6OAyHSHvh6FK z*hh(PLdHnba?D-(s`mYF h|k&ngC z79rqRnFY$SO@f@lQTp4~9o%JPI0b1&elO3)%BttFf5UB4v2h1c`x5uieW#M5@fo99 z$;kX$Zixb5y1kKOgj~wV= Fe>?<4ry&5BBP=Z7t+-I zX0Qm^VEYb%eSi~Gxe6&5=-Gkryg*#wxoQ;x76uQj*&6(>a(yzshhg`vSQ$BQ`=a<` zcrty0QKM^MV7mIA;)yqzIpv&kswdGaS0ks!JAHrB7gyzfY*6nnTZ);r<`Oq}A2?=U zs79+?9?$YI`W?MnGx0<#n+8TQ(L1QNW>rE1fBNWt>}$sddOX1jT*fJQ-Susjaa5X% zDiSApax-~^Q_*K)=~@5%#*mqxPZ68u7)B58E7c =)!{k^7cnKR|ddHtw<|(CL-Y|wAM*MvdJ=n6K%H6)tuWXC|3)3y=^*D zdAp$Nx)OLx=|e5WAg6GlHJ;dia^vgbyn7z-JM(OwlSI6&>q_lyr2 TA?i)PH!3@Mv?#ety{bpY zWDo2#{Nv`3ur01Mv*ni&fD+}7MLlfUb~N#|%%|PC{&%o^QkDFPHU !8s0enAvdr3ptuRMk1=BN~wiFw72o!0c6@T&zTQ6bWR$U5pl5GkG>asF^u z%zTQa$0kXkPJhTobR=Nlj4O*)qOg6=;#3pA27}dr0huo;b-*Z LvmK;sJ(qO`jB5=keSY1c(-9K0fz~d=FeG;i3nE-V(V04KijeWzM zXb~obpufQ1SO{ z9IJ{L(N$UPvT(}VOa7pzP>YZXdqP1geWlBZJf7CA=3Q@GSs*adPvbKJV6Jj{3?qT5 zVC%wvdkKNn+b$qIKUa_xn`PF`da$xI)x&OtUnaPaDk%a^*>A=V1o7Bmz;bxJno%cG z!<^<7mnZt7H=H2=1jcti4C4Ma4P4WwVmCRE5lV_-6pWk;wtxV@l)WfIwQByUkpS6> z;pF(CkRCM|;%&v!3&Uqb1P|<_DFVAH9J`MZtnYC%9Rv&>liTx@W1HXRj?x`kRtJ=Qpf#A(=lq}1kR{lWzqzGer zf$s9t$cMT+m-bhaKsV$lJmvjLnp_5{oVUm{ibT>E>`jPvvY|+1J&irqHK-%4MgoB> zY~T$@S^I=mhmL-rhX%#O EPZzDud>Tgo-Eq?JI`g0f|L1bHJIQ(x!t%G+OMU02j$Dlf~RM$c8# z99qa&{27Q2+G$NF|D8-OKc*tAX7Cm{pGBWVt^@Nq86!4#Yvyy&+*dK%M-?ME#~=$d zMg&;j82oudgWm|Hz8Y>7V44L&5Or5ZZCjH#AYm_`cC*8{LUYk5sjRq|x!6;ix~gUv zmgJ{6rQ$E14b%hX>kDn2o+bgBegI8zP}_WE@=!X@PQ7-OetKfi$*3OMydpt^FDz$i zyMi9$#dqFSF+!8~UDV0eipKO^_^t>&AgKkn^0we`zrn={*d%z`C2;1?%?K^oU7E>? zWtX-7cIo1idZp?Wt^1i4_;%KJUmjOZ0C`Cuv;$Xy`)FN;UWLLQ5oCG=IuZr9B+r2F z0}>NfYDxa@MJrnSB4&5KNPy#<-#x)v@p9PZZdSRZ1MZfl8_a2<+!HD&EWcv);yz1h z&pHg#N1geNI(A)sr$KV4YH%+Dqrvh&A|1nh`4$(>8zeCt#P29k<*0>VQme-+p0v)* z^Xa0@PVe+%bQ(2L`bn{!2P`pZ%Ayo*>b*x+(bACx6H^kqM9R7 o-e>aTx$`MFlNR-mhRuNRxmxZ4PVAd4)Z;g1HX4b4!u!8{?a8HCL8l- z`m!5l(@0HD3ZhP*mJTF0g>X#0%s?p9hHX~70O!OKT-&kNii$-|@R7VcrAqFL(3trj zxB2obq>JvCHSpqSX4F!8oHA|3G NO!TP)*q0E;#jv{M{;oF$|Q5nQPi<}a|m+kjjIN3b4j(-YDUvgJ!mHC1}G`$ zO6j;*8k+_c;!ukjjK5EG@Q$6AS8uqI-)7ZRg5{QhW#d)6$Py;XQ0x-0_4QU7U91-| zY2YcI@)6XsF}qJ!R<%e|HQgD$Tl7{;`{IlKkp;=TFjd)*Q8=^x;|6m}>NRgIy0s1l zz94UGxMSx-t32pZ60SPOaIO 9&^G z2xqLMmT1!%=qU{sSLz;rZptF^QcT}#faQ-!VL-q>JYVNE n+09va_#yH&`SJp!AVT+zGy@cSHb_X{ z6r#6M9v7j4!cxh#n{Vl_vid9XTh|_WHc=i;8)p48Y*W5zwwO1tva7`ro4L9i^01_% zD(ywhab(7faQ9all(Yr>OkBC`37@{PLy{KcXy}7f+~qHMg;@hZ-`{4 ~dyl=sgT(Zb;-fGf2vLJ&TwLHJBb~`7} zn(0;BH>niIj1=X$B99$4 +&@xg{#>NuYljK!B$ggpc&VW zlBM7zF}kjdLDx*uWbgXO(Dn~KllYmTZ6Bz6sXIiV^S6eUV$^trgimwsEMQ@*i5eIO zAsoJ4-Pk3u@!)3lUbt$j#=j~t9N|AfwKvu9y3P +j9VZg) D|2Zw=vC9=M&%RakeBf_7; z&1r S*iR{6vDxpLO`U3 zE2+IHC{6)<>-#jHR!no;f0`sT%vcPHa^x#!@Zqa+taQ^PG|UR?#kUY-;K89#b3OX; z|9!$Dy-7qQQb7Tp7ZFT~cFs-0mY5HT^W_B>7kT~RaB#|@xfy~&U#u*>am9SZ=t29jIpM+z- z6iDT{)AYu#078NAg7#JnUyq765W*?MvXDgb*!n5K^{jb{>)Nf{ni@lIZ&IB~=yG+t zC}m}-vJ!E)R0`2nUE}uTM8Q*?4Vp~aqw_S0L!x+c_B6aU*(F>?6uD@Fs!EV^XT3Z| z^H}_5uFZ6{LWreh;E3G|&ipqdxduJ8&b6R_?k ty2}%#l4fQ) zPZoOdnD9;OEzEnOo51KA<4pr@h?Dmh#1;!;t*F0vRA+?+ex@*Mu<^WwGX~edW7 xRSN>S&THiD7DR(^_ aKgY#rAhhuvL2Lv=qP24RjrIbi3^|8TarF`=2z#q{-Jb z4Q=_1i$J0|2N%#9uk#YGWjGFtE&_(pDj?qk^IU<;fMmQV4?Bf~My8#(^1Atq7r5oo zD<2b8?nZj*0B`ffA~=;>|G7v?WF^T+X#(meOI7w)G%3llSq3PCUUncadIBDX&eh=J zZsZu7HV{6Wcb5&_m di!l^B}d6<~TL&uH{;1 Qe{5V5<@21 z-zyG^e{rL7US;6vqr`0C+6~ zll^H60$- zT(<+`#z&mJKNFdM<)7L{U|Bm{nLQn8ibDRZoNuu-T!rVjAf{Xn^FDU_xQyfb(|wi6 zC`o?(a<=7Cy3M-_`+_zkrLVw#P!z`^QT?~dKn5+z04T&hK6ECf=?sv7a6nBn>3&R$ zj^yq(X%XEl^;^nr9g`;Pl0OQhbW4EYI~Bky^E|6d&;7mKPd-P(PU#o^pe2#?uX`~U zfJQk>WneT@@q&IM?-z6Pce;IFPB-)BM{BYX_d^4{prfXN=1C0`#I}>5>o`qt51% zf70#}1kk!< O`73=R&u<8t4mpD1nS(8|mWxJA^4D-qJGd2z^(DWuh{D%sD#pC<^zRFZ# zd$@Z;nf@;-%yT$QBHNK#c)Dbhmkh&vDeqwL`{}6DkbR}Zl8hv}d#q24<2}jwn3o3# zn~#0EZOkCW$8|S)?Im|O)K+L+8Tvbgl9jwNfrUy{BvGZP*?yfbRa%8CkfkemtZQ^? zRVnX)0mRSPon!9|9 @#LpR8^}8%NS&%TkYW<6g1Lc@n^QG6G>BXA2i1x zE6Jy5(*19tYA<29V*K~{HcXcNO3hVYotofT<=X^9a6g2qDzRdj|4#0o(c!>r(sRgA zZk*MJ-_TQJW=Zzcl2?!w8t|1+(&b>mGikG~Mj)fv_zHCOLma3M>F~y)nep?!%pRMv z`?DTp-KHDOmiYP8TskPGI?m1A;| j*bP{JU7xquPAyf@9hY@$gn17^&v2x+YI)PVksVzEaogQJK+Z?-$`gvc4 zZB+mjbt3D$Bx=0g?9-=qXLxh`xHQ_S>Z-L>L`!Zkq@C>mp_yl>*0pF^hiHeqP3z=$ zlYA|&vJ#^^u7sb^VMUvON`Z(W=tQ~9a$`4MD;Di!iGJC_DBJ}gG?514dz__vb0ek> zjeO}64Qj=Ks A4OO~jZ;$6?>%mZ!wUM$a zw2B>xaG+W}0a%@9Ky1i`X{kuiNOiwl{nC3)xpqNNqN6VoOw28`$m6Vp(bXvHGVGw9 zT2k6A7)6Ig;sw`%L2Jw!{Yf&+c0+8NM$gnM%U*^;UuH2!X;C{m0 zxF#5nP?zpxK67+21PU!evvD<#DoB(!@xHR7gKkFw`x0VE8jC1}W)Gw*mR4H*xV`L@ zIiPEmh4tH9rIqI-1{#;%A*uv@r>`TU-dbriBAhYQoAIU!Z8xrxR18y9AU(f^uNMqZ zLt|aVd*t{j@cP!b=wzjeMX62>c|%S)e8)A_hzqaP(*P~S*Twf7x8PKqzKWl!wNxP= z$Xvdt{r2V<_D#88hUsowWOC2Ve_Y!Y0E`0++Vvt1+=CzSbR?9JaOZwP$Ml+}R&YQ2 zcsOJ13 iR$lI(s!9E#o2bj7p)+7ZiZkS6oR?J}y2LY%UjU>k!f z=6YvGZ0xj;z}ZTt;sJwevv*2St}JNv0Ij%FG`$l$to5!Z8EFfc28%y-Oq?`%<%x&1 zFOm6nl2Gj;(>r2OIF;lmh-BgNs%1ZP1(-fELpgRJr zSgv A%aY2^{e!)Ood3W!tircSgSA(ZUk>>U; z@fm!{y^ETQJ`00>r85>Y_ULRepzmn?2}=zs)lUbv{JM yU`#B&g z$ZE@yW{~J@I~?&uAr7~wek9fZqB2`YcPI@3Fyo3i8?&h^OZ2>ql;ZUl3{-f;%r@vT z{;--$r=*(!7mPk4qE@rW{%LDPSs#FGox%)tFs{y~uuEX!H5VfkdsmJc#K1A}N~@!c z!6I(4%qMXpnNT{`#b <*9Q91*W0v>REeq9r1`qnO1MXT%i$)C}7F*e8O%UY6iNHo#x>0}R zm(RPB!1e~kw#(PM0E=ZY0ZTmqkN9@ArZE8O0f?3arJ9&_Rk9QjRQOE!yzIhxmi2L@ zBQ9U%0+sSww`W8~DeE>q8Jog=Vsf-4zW^hc*{F}|M^u-G3pSPS&SGcHvfi4MP#;P) zsFPSG1$L$X*pRcITXquMwEGjfWA9M<%YE9ECmbW~glT2FC}fn9s1LdF3~7NL=#V=u z!&_(Yr%^@CNi fK@W(GJgKa?k6pQP#i zv8ni?>C5BTm9c6%*5{=< (#IVEkFpNrk@JS(mGm%CC_U0}F)3K+y z6*S#=T o@hC6iW(%%3<=u5PTrPy%Y7THxs_ zCFg)P93 qgk @3TJRJgyR^|d7w`(d5!EJc5fi=Y;I-lM) zJNmVfINipxTg=j$NDMV=-lQYJ6(ZDGD)FC_t6Hg-&P<=#2Wb1aZ({K4C3Th`lbI z1D~A8Dyyx0phFw$bawS50g0c4g3gK#E=w}DPAjuCKcblRGzB#pkxkHp4M?LhGeK3u zy9`VJ*E 0?K|EGFPcMf=-Ko{YpN-?H`%K)iU`l?;?}dW(8wQCMWoq zx-!~Ok#jIO(u77n$nh{YH5rf0wt?t0ZDmke(l2r6NG)Ys(EIU^BN*Sr>*VYPNB)eL z?7ck;8Dh1pxH*yzF;}CSi<8Pbr|o+qEUpUIWX_qSN(gI2^f~cy1;WB=+D?PUzTK%T zmz$heOt;ceq6#@SXY;OCPktkeO;EBSTJLdt$PtaLo-7JMkPPU}Pi13`D!BS0o1WvL z&2NgHjGfMSO>5y*q*E6kzBVN_h?zCwnKPfPp_26((VL@K)qPV3ljs5EG!VP?-a<`` z5vG}_Zda;dJnvp{%xqM%_&Ahy{Cr%Qr Uu>?dv6nd^;^P_WKaN14 zJ9Sy#`v~S@U`9}?0XC4akPu}b!QYOVn`bj32p4#*#}2Ob64`3AFKttkZ}97-$!{yh zp9V_8%)D!Htl$r%Fe7bHGc&9vi7EI72xZLBwSpj>OaYM`W7Xu$PAJ)HAz%qK7<*=l z(N&+Ol3~*llI9ai)B0W=F~DJft&=S*QDCeLBnkBt1f0X)*Nl~%Yqd^S5#;K^x7sDv za1q$hj&<5`n_u<