From 502d909459f0984be1b915874fccdfa8b0dccf0c Mon Sep 17 00:00:00 2001 From: h Date: Wed, 8 Mar 2023 13:19:01 +0800 Subject: [PATCH 01/27] support config tags to translate --- book_maker/cli.py | 8 ++++++++ book_maker/loader/base_loader.py | 17 ++--------------- book_maker/loader/epub_loader.py | 7 +++++-- test_books/Liber_Esther.epub | Bin 0 -> 82411 bytes 4 files changed, 15 insertions(+), 17 deletions(-) create mode 100644 test_books/Liber_Esther.epub diff --git a/book_maker/cli.py b/book_maker/cli.py index 1243afbc..d9739f0a 100644 --- a/book_maker/cli.py +++ b/book_maker/cli.py @@ -82,6 +82,13 @@ def main(): type=str, help="replace base url from openapi", ) + parser.add_argument( + "--translate-tags", + dest="translate_tags", + type=str, + default="p", + help="example --translate-tags p,blockquote", + ) options = parser.parse_args() PROXY = options.proxy @@ -119,6 +126,7 @@ def main(): model_api_base=model_api_base, is_test=options.test, test_num=options.test_num, + translate_tags=options.translate_tags, ) e.make_bilingual_book() diff --git a/book_maker/loader/base_loader.py b/book_maker/loader/base_loader.py index 46986ffe..ed6040de 100644 --- a/book_maker/loader/base_loader.py +++ b/book_maker/loader/base_loader.py @@ -1,20 +1,7 @@ -from abc import abstractmethod +from abc import ABC, abstractmethod -class BaseBookLoader: - def __init__( - self, - epub_name, - model, - key, - resume, - language, - model_api_base=None, - is_test=False, - test_num=5, - ): - pass - +class BaseBookLoader(ABC): @staticmethod def _is_special_text(text): return text.isdigit() or text.isspace() diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index ee1127de..5f66721e 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -24,12 +24,14 @@ def __init__( model_api_base=None, is_test=False, test_num=5, + translate_tags="p", ): self.epub_name = epub_name self.new_epub = epub.EpubBook() self.translate_model = model(key, language, model_api_base) self.is_test = is_test self.test_num = test_num + self.translate_tags = translate_tags try: self.origin_book = epub.read_epub(self.epub_name) @@ -69,10 +71,11 @@ def _make_new_book(self, book): def make_bilingual_book(self): new_book = self._make_new_book(self.origin_book) all_items = list(self.origin_book.get_items()) + trans_taglist = self.translate_tags.split(",") all_p_length = sum( 0 if i.get_type() != ITEM_DOCUMENT - else len(bs(i.content, "html.parser").findAll("p")) + else len(bs(i.content, "html.parser").findAll(trans_taglist)) for i in all_items ) pbar = tqdm(total=self.test_num) if self.is_test else tqdm(total=all_p_length) @@ -82,7 +85,7 @@ def make_bilingual_book(self): for item in self.origin_book.get_items(): if item.get_type() == ITEM_DOCUMENT: soup = bs(item.content, "html.parser") - p_list = soup.findAll("p") + p_list = soup.findAll(trans_taglist) is_test_done = self.is_test and index > self.test_num for p in p_list: if is_test_done or not p.text or self._is_special_text(p.text): diff --git a/test_books/Liber_Esther.epub b/test_books/Liber_Esther.epub new file mode 100644 index 0000000000000000000000000000000000000000..515886c92264c9de742fe4d67629bd9ca0c07301 GIT binary patch literal 82411 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 literal 0 HcmV?d00001 From 3277eefc4ed0330158edce08499f5e4beb5548a7 Mon Sep 17 00:00:00 2001 From: h Date: Sat, 11 Mar 2023 12:20:42 +0800 Subject: [PATCH 02/27] support system meesage in envirment --- book_maker/translator/chatgptapi_translator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index dfdfdbc1..0dbd6c4a 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -1,6 +1,7 @@ import time import openai +from os import environ from .base_translator import Base @@ -20,10 +21,14 @@ def get_translation(self, text): completion = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ + { + "role": "system", + "content": environ.get("OPENAI_API_SYS_MSG"), + }, { "role": "user", "content": f"Please help me to translate,`{text}` to {self.language}, please return only translated content not include the origin text", - } + }, ], ) t_text = ( From f39b926fc6d9bbd2dca5d497310a721994c7caf5 Mon Sep 17 00:00:00 2001 From: h Date: Sat, 11 Mar 2023 21:20:01 +0800 Subject: [PATCH 03/27] cumulative translation --- book_maker/cli.py | 8 +++ book_maker/loader/epub_loader.py | 69 +++++++++++++++++-- book_maker/loader/txt_loader.py | 1 + .../translator/chatgptapi_translator.py | 10 +++ 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/book_maker/cli.py b/book_maker/cli.py index 95bb6b65..d82d4b3a 100644 --- a/book_maker/cli.py +++ b/book_maker/cli.py @@ -90,6 +90,13 @@ def main(): default=False, help="allow NavigableStrings to be translated", ) + parser.add_argument( + "--accumulated_num", + dest="accumulated_num", + type=int, + default=1, + help="Wait for how many characters have been accumulated before starting the translation", + ) options = parser.parse_args() PROXY = options.proxy @@ -136,6 +143,7 @@ def main(): test_num=options.test_num, translate_tags=options.translate_tags, allow_navigable_strings=options.allow_navigable_strings, + accumulated_num=options.accumulated_num, ) e.make_bilingual_book() diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index 3f04dde0..f3917f9d 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -25,6 +25,7 @@ def __init__( test_num=5, translate_tags="p", allow_navigable_strings=False, + accumulated_num=1, ): self.epub_name = epub_name self.new_epub = epub.EpubBook() @@ -33,6 +34,7 @@ def __init__( self.test_num = test_num self.translate_tags = translate_tags self.allow_navigable_strings = allow_navigable_strings + self.accumulated_num = accumulated_num try: self.origin_book = epub.read_epub(self.epub_name) @@ -70,6 +72,28 @@ def _make_new_book(self, book): return new_book def make_bilingual_book(self): + def deal_new(p, waitPList): + ret = deal_old(waitPList) + new_p = copy(p) + new_p.string = self.translate_model.translate(p.text) + p.insert_after(new_p) + return ret + + def deal_old(waitPList): + if len(waitPList) == 0: + return [] + + resultTxtList = self.translate_model.translate_list(waitPList) + + for i in range(0, len(waitPList)): + if i < len(resultTxtList): + p = waitPList[i] + new_p = copy(p) + new_p.string = resultTxtList[i] + p.insert_after(new_p) + + return [] + new_book = self._make_new_book(self.origin_book) all_items = list(self.origin_book.get_items()) trans_taglist = self.translate_tags.split(",") @@ -89,12 +113,44 @@ def make_bilingual_book(self): index = 0 p_to_save_len = len(self.p_to_save) try: + # Add the things that don't need to be translated first, so that you can see the img after the interruption for item in self.origin_book.get_items(): - if item.get_type() == ITEM_DOCUMENT: - soup = bs(item.content, "html.parser") - p_list = soup.findAll(trans_taglist) - if self.allow_navigable_strings: - p_list.extend(soup.findAll(text=True)) + if item.get_type() != ITEM_DOCUMENT: + new_book.add_item(item) + + for item in self.origin_book.get_items_of_type(ITEM_DOCUMENT): + soup = bs(item.content, "html.parser") + p_list = soup.findAll(trans_taglist) + if self.allow_navigable_strings: + p_list.extend(soup.findAll(text=True)) + + sendNum = self.accumulated_num + if sendNum > 1: + count = 0 + waitPList = [] + for i in range(0, len(p_list)): + p = p_list[i] + if not p.text or self._is_special_text(p.text): + continue + length = len(p.text) + if length > sendNum: + waitPList = deal_new(p, waitPList) + continue + if i == len(p_list) - 1: + if count + length < sendNum: + waitPList.append(p) + waitPList = deal_old(waitPList) + else: + waitPList = deal_new(p, waitPList) + break + if count + length < sendNum: + count += length + waitPList.append(p) + else: + waitPList = deal_old(waitPList) + waitPList.append(p) + count = len(p.text) + else: is_test_done = self.is_test and index > self.test_num for p in p_list: if is_test_done or not p.text or self._is_special_text(p.text): @@ -115,7 +171,8 @@ def make_bilingual_book(self): pbar.update(1) if self.is_test and index >= self.test_num: break - item.content = soup.prettify().encode() + + item.content = soup.prettify().encode() new_book.add_item(item) name, _ = os.path.splitext(self.epub_name) epub.write_epub(f"{name}_bilingual.epub", new_book, {}) diff --git a/book_maker/loader/txt_loader.py b/book_maker/loader/txt_loader.py index 72cd31cf..0c1c8a7c 100644 --- a/book_maker/loader/txt_loader.py +++ b/book_maker/loader/txt_loader.py @@ -17,6 +17,7 @@ def __init__( model_api_base=None, is_test=False, test_num=5, + accumulated_num=1, ): self.txt_name = txt_name self.translate_model = model(key, language, model_api_base) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index 0dbd6c4a..643e3f74 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -60,3 +60,13 @@ def translate(self, text): # todo: Determine whether to print according to the cli option print(t_text) return t_text + + def translate_list(self, plist): + sep = "\n\n\n\n\n" + new_str = sep.join([item.text for item in plist]) + resultStr = self.translate(new_str) + + lines = resultStr.split("\n") + lines = [line.strip() for line in lines if line.strip() != ""] + + return lines From 59ed6d96a8de001294a08ec607b2ed3a78e2ecf9 Mon Sep 17 00:00:00 2001 From: h Date: Sat, 11 Mar 2023 21:30:36 +0800 Subject: [PATCH 04/27] fix --- book_maker/translator/chatgptapi_translator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index 643e3f74..7bd00910 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -23,7 +23,7 @@ def get_translation(self, text): messages=[ { "role": "system", - "content": environ.get("OPENAI_API_SYS_MSG"), + "content": environ.get("OPENAI_API_SYS_MSG") or "", }, { "role": "user", From 2baf35918b4b63f4b0f4882afc4e3fb09674ca73 Mon Sep 17 00:00:00 2001 From: h Date: Sat, 11 Mar 2023 22:10:04 +0800 Subject: [PATCH 05/27] fix --- book_maker/cli.py | 7 +++ book_maker/loader/epub_loader.py | 44 +++++++++---------- .../translator/chatgptapi_translator.py | 4 +- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/book_maker/cli.py b/book_maker/cli.py index 9a67b0fe..952b66ee 100644 --- a/book_maker/cli.py +++ b/book_maker/cli.py @@ -129,6 +129,13 @@ def main(): metavar="PROMPT_TEMPLATE", help="used for customizing the prompt. It can be the prompt template string, or a path to the template file. The valid placeholders are `{text}` and `{language}`.", ) + parser.add_argument( + "--accumulated_num", + dest="accumulated_num", + type=int, + default=1, + help="Wait for how many characters have been accumulated before starting the translation", + ) options = parser.parse_args() PROXY = options.proxy diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index 644051dc..4fa8288d 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -75,24 +75,24 @@ def _make_new_book(self, book): return new_book def make_bilingual_book(self): - def deal_new(p, waitPList): - ret = deal_old(waitPList) + def deal_new(p, wait_p_list): + ret = deal_old(wait_p_list) new_p = copy(p) new_p.string = self.translate_model.translate(p.text) p.insert_after(new_p) return ret - def deal_old(waitPList): - if len(waitPList) == 0: + def deal_old(wait_p_list): + if len(wait_p_list) == 0: return [] - resultTxtList = self.translate_model.translate_list(waitPList) + result_txt_list = self.translate_model.translate_list(wait_p_list) - for i in range(0, len(waitPList)): - if i < len(resultTxtList): - p = waitPList[i] + for i in range(0, len(wait_p_list)): + if i < len(result_txt_list): + p = wait_p_list[i] new_p = copy(p) - new_p.string = resultTxtList[i] + new_p.string = result_txt_list[i] p.insert_after(new_p) return [] @@ -127,31 +127,31 @@ def deal_old(waitPList): if self.allow_navigable_strings: p_list.extend(soup.findAll(text=True)) - sendNum = self.accumulated_num - if sendNum > 1: + send_num = self.accumulated_num + if send_num > 1: count = 0 - waitPList = [] + wait_p_list = [] for i in range(0, len(p_list)): p = p_list[i] if not p.text or self._is_special_text(p.text): continue length = len(p.text) - if length > sendNum: - waitPList = deal_new(p, waitPList) + if length > send_num: + wait_p_list = deal_new(p, wait_p_list) continue if i == len(p_list) - 1: - if count + length < sendNum: - waitPList.append(p) - waitPList = deal_old(waitPList) + if count + length < send_num: + wait_p_list.append(p) + wait_p_list = deal_old(wait_p_list) else: - waitPList = deal_new(p, waitPList) + wait_p_list = deal_new(p, wait_p_list) break - if count + length < sendNum: + if count + length < send_num: count += length - waitPList.append(p) + wait_p_list.append(p) else: - waitPList = deal_old(waitPList) - waitPList.append(p) + wait_p_list = deal_old(wait_p_list) + wait_p_list.append(p) count = len(p.text) else: is_test_done = self.is_test and index > self.test_num diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index 8344ebf9..e751e217 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -70,9 +70,9 @@ def translate(self, text): def translate_list(self, plist): sep = "\n\n\n\n\n" new_str = sep.join([item.text for item in plist]) - resultStr = self.translate(new_str) + result_str = self.translate(new_str) - lines = resultStr.split("\n") + lines = result_str.split("\n") lines = [line.strip() for line in lines if line.strip() != ""] return lines From 3523469e570a13bfd168dec0830c7356226328b8 Mon Sep 17 00:00:00 2001 From: h Date: Sat, 11 Mar 2023 23:27:26 +0800 Subject: [PATCH 06/27] clean --- book_maker/loader/epub_loader.py | 17 +++++++++++------ book_maker/translator/chatgptapi_translator.py | 5 +++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index 4fa8288d..4df00bbf 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -129,6 +129,9 @@ def deal_old(wait_p_list): send_num = self.accumulated_num if send_num > 1: + print("------------------------------------------------------") + print(f"dealing {item.file_name} ...") + print("------------------------------------------------------") count = 0 wait_p_list = [] for i in range(0, len(p_list)): @@ -177,14 +180,16 @@ def deal_old(wait_p_list): item.content = soup.prettify().encode() new_book.add_item(item) - name, _ = os.path.splitext(self.epub_name) - epub.write_epub(f"{name}_bilingual.epub", new_book, {}) - pbar.close() + name, _ = os.path.splitext(self.epub_name) + epub.write_epub(f"{name}_bilingual.epub", new_book, {}) + if self.accumulated_num == 1: + pbar.close() except (KeyboardInterrupt, Exception) as e: print(e) - print("you can resume it next time") - self._save_progress() - self._save_temp_book() + if self.accumulated_num == 1: + print("you can resume it next time") + self._save_progress() + self._save_temp_book() sys.exit(0) def load_state(self): diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index e751e217..da204125 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -1,4 +1,5 @@ import time +import re import openai from os import environ @@ -48,7 +49,7 @@ def get_translation(self, text): def translate(self, text): # todo: Determine whether to print according to the cli option - print(text) + print(re.sub("\n{3,}", "\n\n", text)) try: t_text = self.get_translation(text) @@ -64,7 +65,7 @@ def translate(self, text): t_text = self.get_translation(text) # todo: Determine whether to print according to the cli option - print(t_text.strip()) + print(re.sub("\n{3,}", "\n\n", t_text)) return t_text def translate_list(self, plist): From 9aade4b815c950f80a9a889282ebcc44709c888d Mon Sep 17 00:00:00 2001 From: h Date: Sun, 12 Mar 2023 05:03:38 +0800 Subject: [PATCH 07/27] prompt and retry --- book_maker/loader/epub_loader.py | 1 - .../translator/chatgptapi_translator.py | 69 ++++++++++++++++--- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index 4df00bbf..ff18dd39 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -131,7 +131,6 @@ def deal_old(wait_p_list): if send_num > 1: print("------------------------------------------------------") print(f"dealing {item.file_name} ...") - print("------------------------------------------------------") count = 0 wait_p_list = [] for i in range(0, len(p_list)): diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index da204125..b2e823ba 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -18,11 +18,14 @@ def __init__(self, key, language, api_base=None, prompt_template=None): or "Please help me to translate,`{text}` to {language}, please return only translated content not include the origin text" ) + max_num_token = -1 + def rotate_key(self): openai.api_key = next(self.keys) def get_translation(self, text): self.rotate_key() + content = self.prompt_template.format(text=text, language=self.language) completion = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ @@ -32,9 +35,7 @@ def get_translation(self, text): }, { "role": "user", - "content": self.prompt_template.format( - text=text, language=self.language - ), + "content": content, }, ], ) @@ -45,11 +46,18 @@ def get_translation(self, text): .encode("utf8") .decode() ) + print("=================================================") + print(f'Total tokens used this time: {completion["usage"]["total_tokens"]}') + self.max_num_token = max( + self.max_num_token, int(completion["usage"]["total_tokens"]) + ) + print(f"The maximum number of tokens used at one time: {self.max_num_token}") return t_text - def translate(self, text): + def translate(self, text, needprint=True): # todo: Determine whether to print according to the cli option - print(re.sub("\n{3,}", "\n\n", text)) + if needprint: + print(re.sub("\n{3,}", "\n\n", text)) try: t_text = self.get_translation(text) @@ -65,15 +73,58 @@ def translate(self, text): t_text = self.get_translation(text) # todo: Determine whether to print according to the cli option - print(re.sub("\n{3,}", "\n\n", t_text)) + if needprint: + print(re.sub("\n{3,}", "\n\n", t_text)) return t_text + def translate_and_split_lines(self, text): + result_str = self.translate(text, False) + lines = result_str.split("\n") + lines = [line.strip() for line in lines if line.strip() != ""] + return lines + def translate_list(self, plist): sep = "\n\n\n\n\n" new_str = sep.join([item.text for item in plist]) - result_str = self.translate(new_str) - lines = result_str.split("\n") - lines = [line.strip() for line in lines if line.strip() != ""] + retry_count = 0 + plist_len = len(plist) + + # supplement_prompt = f"Translated result should have {plist_len} paragraphs" + # supplement_prompt = "Each paragraph in the source text should be translated into a separate and complete paragraph, and each paragraph should be separated" + supplement_prompt = "Each paragraph in the source text should be translated into a separate and complete paragraph, and each translated paragraph should be separated by a blank line" + + self.prompt_template = ( + "Please help me to translate,`{text}` to {language}, please return only translated content not include the origin text. " + + supplement_prompt + ) + + lines = self.translate_and_split_lines(new_str) + + while len(lines) != plist_len and retry_count < 15: + print( + f"bug: {plist_len} paragraphs of text translated into {len(lines)} paragraphs" + ) + num = 6 + print(f"sleep for {num}s and try again") + time.sleep(num) + print(f"retry {retry_count+1} ...") + lines = self.translate_and_split_lines(new_str) + retry_count += 1 + if len(lines) == plist_len: + print("retry success") + + if len(lines) != plist_len: + for i in range(0, plist_len): + print(plist[i].text) + print() + if i < len(lines): + print(lines[i]) + print() + + print( + f"bug: {plist_len} paragraphs of text translated into {len(lines)} paragraphs" + ) + print("continue") return lines From 8badb4153ef72945a70939efcef9a086a58775ab Mon Sep 17 00:00:00 2001 From: h Date: Sun, 12 Mar 2023 13:14:34 +0800 Subject: [PATCH 08/27] improve prompt and fix change output --- book_maker/loader/epub_loader.py | 3 + .../translator/chatgptapi_translator.py | 74 ++++++++++++------- 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index ff18dd39..1746c7ca 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -122,6 +122,9 @@ def deal_old(wait_p_list): new_book.add_item(item) for item in self.origin_book.get_items_of_type(ITEM_DOCUMENT): + # if item.file_name != "OEBPS/ch01.xhtml": + # continue + soup = bs(item.content, "html.parser") p_list = soup.findAll(trans_taglist) if self.allow_navigable_strings: diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index b2e823ba..145cbb75 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -17,6 +17,7 @@ def __init__(self, key, language, api_base=None, prompt_template=None): prompt_template or "Please help me to translate,`{text}` to {language}, please return only translated content not include the origin text" ) + self.system_content = environ.get("OPENAI_API_SYS_MSG") or "" max_num_token = -1 @@ -31,7 +32,7 @@ def get_translation(self, text): messages=[ { "role": "system", - "content": environ.get("OPENAI_API_SYS_MSG") or "", + "content": self.system_content, }, { "role": "user", @@ -47,11 +48,12 @@ def get_translation(self, text): .decode() ) print("=================================================") - print(f'Total tokens used this time: {completion["usage"]["total_tokens"]}') self.max_num_token = max( self.max_num_token, int(completion["usage"]["total_tokens"]) ) - print(f"The maximum number of tokens used at one time: {self.max_num_token}") + print( + f"{completion['usage']['total_tokens']} {completion['usage']['prompt_tokens']} {completion['usage']['completion_tokens']} {self.max_num_token} (total_token, prompt_token, completion_tokens, max_history_total_token)" + ) return t_text def translate(self, text, needprint=True): @@ -85,42 +87,62 @@ def translate_and_split_lines(self, text): def translate_list(self, plist): sep = "\n\n\n\n\n" - new_str = sep.join([item.text for item in plist]) + # new_str = sep.join([item.text for item in plist]) + + new_str = "" + for p in plist: + for sup in p.find_all("sup"): + sup.extract() # 提取并删除标签及其内容, 但不影响p + new_str += p.get_text().strip() + "\n\n" + + if new_str.endswith(sep): + new_str = new_str[: -len(sep)] - retry_count = 0 plist_len = len(plist) + self.system_content += f"""Please translate the following paragraphs individually while preserving their original structure(This time it should be exactly {plist_len} paragraphs, no more or less). Only translate the paragraphs provided below: - # supplement_prompt = f"Translated result should have {plist_len} paragraphs" - # supplement_prompt = "Each paragraph in the source text should be translated into a separate and complete paragraph, and each paragraph should be separated" - supplement_prompt = "Each paragraph in the source text should be translated into a separate and complete paragraph, and each translated paragraph should be separated by a blank line" +[Insert first paragraph here] - self.prompt_template = ( - "Please help me to translate,`{text}` to {language}, please return only translated content not include the origin text. " - + supplement_prompt - ) +[Insert second paragraph here] +[Insert third paragraph here]""" + + retry_count = 0 lines = self.translate_and_split_lines(new_str) - while len(lines) != plist_len and retry_count < 15: + while len(lines) != plist_len and retry_count < 3: print( - f"bug: {plist_len} paragraphs of text translated into {len(lines)} paragraphs" + f"bug: {plist_len} -> {len(lines)} : Number of paragraphs before and after translation" ) - num = 6 - print(f"sleep for {num}s and try again") - time.sleep(num) - print(f"retry {retry_count+1} ...") + sleep_dur = 6 + print(f"sleep for {sleep_dur}s and retry {retry_count+1} ...") + time.sleep(sleep_dur) lines = self.translate_and_split_lines(new_str) retry_count += 1 - if len(lines) == plist_len: - print("retry success") + + state = "success" + if len(lines) != plist_len: + state = "fail" + + if retry_count > 0: + print(f"retry {state}") + with open("buglog.txt", "a") as f: + print( + f"retry {state}, count = {retry_count}", + file=f, + ) if len(lines) != plist_len: - for i in range(0, plist_len): - print(plist[i].text) - print() - if i < len(lines): - print(lines[i]) - print() + newlist = new_str.split(sep) + for i in range(0, len(newlist)): + with open("buglog.txt", "a") as f: + print(newlist[i], file=f) + print(file=f) + if i < len(lines): + print(lines[i], file=f) + print(file=f) + print("=============================", file=f) + print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") print( f"bug: {plist_len} paragraphs of text translated into {len(lines)} paragraphs" From b0d4f8663839651a64a626bc4943d84b2ff3d908 Mon Sep 17 00:00:00 2001 From: h Date: Sun, 12 Mar 2023 21:50:29 +0800 Subject: [PATCH 09/27] clean, fix link translate --- book_maker/loader/epub_loader.py | 27 ++++++++++++++-- .../translator/chatgptapi_translator.py | 32 ++++++++++--------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index 1746c7ca..71f63284 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -1,4 +1,5 @@ import os +import re import pickle import sys from copy import copy @@ -12,6 +13,21 @@ from .base_loader import BaseBookLoader +def isLink(text): + url_pattern = re.compile( + r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" + ) + return bool(url_pattern.match(text.strip())) + + +def isSourceLink(text): + text = text.strip() + return text.startswith("Source: ") and re.search( + r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", + text, + ) + + class EPUBBookLoader(BaseBookLoader): def __init__( self, @@ -65,7 +81,7 @@ def _load_spine(self): @staticmethod def _is_special_text(text): - return text.isdigit() or text.isspace() + return text.isdigit() or text.isspace() or isLink(text) def _make_new_book(self, book): new_book = epub.EpubBook() @@ -138,7 +154,14 @@ def deal_old(wait_p_list): wait_p_list = [] for i in range(0, len(p_list)): p = p_list[i] - if not p.text or self._is_special_text(p.text): + temp_p = copy(p) + for sup in temp_p.find_all("sup"): + sup.extract() + if ( + not p.text + or self._is_special_text(temp_p.text) + or isSourceLink(temp_p.text) + ): continue length = len(p.text) if length > send_num: diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index 145cbb75..f8b7ba59 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -1,5 +1,6 @@ import time import re +from copy import copy import openai from os import environ @@ -91,9 +92,10 @@ def translate_list(self, plist): new_str = "" for p in plist: - for sup in p.find_all("sup"): - sup.extract() # 提取并删除标签及其内容, 但不影响p - new_str += p.get_text().strip() + "\n\n" + temp_p = copy(p) + for sup in temp_p.find_all("sup"): + sup.extract() + new_str += temp_p.get_text().strip() + sep if new_str.endswith(sep): new_str = new_str[: -len(sep)] @@ -108,20 +110,20 @@ def translate_list(self, plist): [Insert third paragraph here]""" retry_count = 0 - lines = self.translate_and_split_lines(new_str) + result_list = self.translate_and_split_lines(new_str) - while len(lines) != plist_len and retry_count < 3: + while len(result_list) != plist_len and retry_count < 3: print( - f"bug: {plist_len} -> {len(lines)} : Number of paragraphs before and after translation" + f"bug: {plist_len} -> {len(result_list)} : Number of paragraphs before and after translation" ) sleep_dur = 6 print(f"sleep for {sleep_dur}s and retry {retry_count+1} ...") time.sleep(sleep_dur) - lines = self.translate_and_split_lines(new_str) + result_list = self.translate_and_split_lines(new_str) retry_count += 1 state = "success" - if len(lines) != plist_len: + if len(result_list) != plist_len: state = "fail" if retry_count > 0: @@ -132,21 +134,21 @@ def translate_list(self, plist): file=f, ) - if len(lines) != plist_len: + if len(result_list) != plist_len: newlist = new_str.split(sep) - for i in range(0, len(newlist)): - with open("buglog.txt", "a") as f: + with open("buglog.txt", "a") as f: + for i in range(0, len(newlist)): print(newlist[i], file=f) print(file=f) - if i < len(lines): - print(lines[i], file=f) + if i < len(result_list): + print(result_list[i], file=f) print(file=f) print("=============================", file=f) print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") print( - f"bug: {plist_len} paragraphs of text translated into {len(lines)} paragraphs" + f"bug: {plist_len} paragraphs of text translated into {len(result_list)} paragraphs" ) print("continue") - return lines + return result_list From 46320b45393c8b25dc19a3f0fe440ef2e88be489 Mon Sep 17 00:00:00 2001 From: h Date: Mon, 13 Mar 2023 01:13:58 +0800 Subject: [PATCH 10/27] clean --- book_maker/loader/epub_loader.py | 19 +++++++++---------- book_maker/loader/txt_loader.py | 2 +- .../translator/chatgptapi_translator.py | 6 ++---- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index 71f63284..1d2e0214 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -92,26 +92,25 @@ def _make_new_book(self, book): def make_bilingual_book(self): def deal_new(p, wait_p_list): - ret = deal_old(wait_p_list) + deal_old(wait_p_list) new_p = copy(p) new_p.string = self.translate_model.translate(p.text) p.insert_after(new_p) - return ret def deal_old(wait_p_list): if len(wait_p_list) == 0: - return [] + return result_txt_list = self.translate_model.translate_list(wait_p_list) - for i in range(0, len(wait_p_list)): + for i in range(len(wait_p_list)): if i < len(result_txt_list): p = wait_p_list[i] new_p = copy(p) new_p.string = result_txt_list[i] p.insert_after(new_p) - return [] + wait_p_list.clear() new_book = self._make_new_book(self.origin_book) all_items = list(self.origin_book.get_items()) @@ -152,7 +151,7 @@ def deal_old(wait_p_list): print(f"dealing {item.file_name} ...") count = 0 wait_p_list = [] - for i in range(0, len(p_list)): + for i in range(len(p_list)): p = p_list[i] temp_p = copy(p) for sup in temp_p.find_all("sup"): @@ -165,20 +164,20 @@ def deal_old(wait_p_list): continue length = len(p.text) if length > send_num: - wait_p_list = deal_new(p, wait_p_list) + deal_new(p, wait_p_list) continue if i == len(p_list) - 1: if count + length < send_num: wait_p_list.append(p) - wait_p_list = deal_old(wait_p_list) + deal_old(wait_p_list) else: - wait_p_list = deal_new(p, wait_p_list) + deal_new(p, wait_p_list) break if count + length < send_num: count += length wait_p_list.append(p) else: - wait_p_list = deal_old(wait_p_list) + deal_old(wait_p_list) wait_p_list.append(p) count = len(p.text) else: diff --git a/book_maker/loader/txt_loader.py b/book_maker/loader/txt_loader.py index 5a8fe421..c625cea6 100644 --- a/book_maker/loader/txt_loader.py +++ b/book_maker/loader/txt_loader.py @@ -80,7 +80,7 @@ def make_bilingual_book(self): def _save_temp_book(self): index = 0 - for i in range(0, len(self.origin_book)): + for i in range(len(self.origin_book)): self.bilingual_temp_result.append(self.origin_book[i]) if self._is_special_text(self.origin_book[i]): continue diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index f8b7ba59..fdd2a597 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -110,21 +110,19 @@ def translate_list(self, plist): [Insert third paragraph here]""" retry_count = 0 + sleep_dur = 6 result_list = self.translate_and_split_lines(new_str) while len(result_list) != plist_len and retry_count < 3: print( f"bug: {plist_len} -> {len(result_list)} : Number of paragraphs before and after translation" ) - sleep_dur = 6 print(f"sleep for {sleep_dur}s and retry {retry_count+1} ...") time.sleep(sleep_dur) result_list = self.translate_and_split_lines(new_str) retry_count += 1 - state = "success" - if len(result_list) != plist_len: - state = "fail" + state = "fail" if len(result_list) != plist_len else "success" if retry_count > 0: print(f"retry {state}") From 05466475cc1b9ee6eeae9900c5eba9ce7ac32cf4 Mon Sep 17 00:00:00 2001 From: h Date: Mon, 13 Mar 2023 03:36:13 +0800 Subject: [PATCH 11/27] more prompt, exclude Listing, change output --- book_maker/loader/epub_loader.py | 6 ++++ .../translator/chatgptapi_translator.py | 31 +++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index 1d2e0214..d42d8164 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -28,6 +28,11 @@ def isSourceLink(text): ) +def isList(text, num=80): + text = text.strip() + return re.match(r"^Listing\s*\d+", text) and len(text) < num + + class EPUBBookLoader(BaseBookLoader): def __init__( self, @@ -160,6 +165,7 @@ def deal_old(wait_p_list): not p.text or self._is_special_text(temp_p.text) or isSourceLink(temp_p.text) + or isList(temp_p.text) ): continue length = len(p.text) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index fdd2a597..dbcc9d54 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -49,12 +49,18 @@ def get_translation(self, text): .decode() ) print("=================================================") - self.max_num_token = max( - self.max_num_token, int(completion["usage"]["total_tokens"]) - ) - print( - f"{completion['usage']['total_tokens']} {completion['usage']['prompt_tokens']} {completion['usage']['completion_tokens']} {self.max_num_token} (total_token, prompt_token, completion_tokens, max_history_total_token)" - ) + if int(completion["usage"]["total_tokens"]) > self.max_num_token: + self.max_num_token = int(completion["usage"]["total_tokens"]) + print( + f"The current largest total number of tokens update: {self.max_num_token}" + ) + + # self.max_num_token = max( + # self.max_num_token, int(completion["usage"]["total_tokens"]) + # ) + # print( + # f"{completion['usage']['total_tokens']} {completion['usage']['prompt_tokens']} {completion['usage']['completion_tokens']} {self.max_num_token} (total_token, prompt_token, completion_tokens, max_history_total_token)" + # ) return t_text def translate(self, text, needprint=True): @@ -101,19 +107,24 @@ def translate_list(self, plist): new_str = new_str[: -len(sep)] plist_len = len(plist) - self.system_content += f"""Please translate the following paragraphs individually while preserving their original structure(This time it should be exactly {plist_len} paragraphs, no more or less). Only translate the paragraphs provided below: + always_trans = """[If there are any links, images, figure, listing or other content that cannot be translated, please leave them in the original language. If you cannot translate the content, please include it in brackets like this]: +[Insert Original Content Here] + """ + + self.system_content += f"""Please translate the following paragraphs individually while preserving their original structure(This time it should be exactly {plist_len} paragraphs, no more or less). {always_trans}. Only translate the paragraphs provided below: [Insert first paragraph here] [Insert second paragraph here] -[Insert third paragraph here]""" +[Insert third paragraph here] +""" retry_count = 0 sleep_dur = 6 result_list = self.translate_and_split_lines(new_str) - while len(result_list) != plist_len and retry_count < 3: + while len(result_list) != plist_len and retry_count < 5: print( f"bug: {plist_len} -> {len(result_list)} : Number of paragraphs before and after translation" ) @@ -133,8 +144,10 @@ def translate_list(self, plist): ) if len(result_list) != plist_len: + # todo: select best newlist = new_str.split(sep) with open("buglog.txt", "a") as f: + print(f"problem size: {plist_len - len(result_list)}", file=f) for i in range(0, len(newlist)): print(newlist[i], file=f) print(file=f) From 4be8dc40b4aae91203708d5642ce0fac4d80c556 Mon Sep 17 00:00:00 2001 From: h Date: Mon, 13 Mar 2023 12:48:55 +0800 Subject: [PATCH 12/27] deal exception: ["finish_reason"] == "length" exclude all "Source: " --- book_maker/loader/epub_loader.py | 10 +-- .../translator/chatgptapi_translator.py | 70 +++++++++++-------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index d42d8164..d50b8385 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -20,12 +20,8 @@ def isLink(text): return bool(url_pattern.match(text.strip())) -def isSourceLink(text): - text = text.strip() - return text.startswith("Source: ") and re.search( - r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", - text, - ) +def isSource(text): + return text.strip().startswith("Source: ") def isList(text, num=80): @@ -164,7 +160,7 @@ def deal_old(wait_p_list): if ( not p.text or self._is_special_text(temp_p.text) - or isSourceLink(temp_p.text) + or isSource(temp_p.text) or isList(temp_p.text) ): continue diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index dbcc9d54..947da8e3 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -28,39 +28,47 @@ def rotate_key(self): def get_translation(self, text): self.rotate_key() content = self.prompt_template.format(text=text, language=self.language) - completion = openai.ChatCompletion.create( - model="gpt-3.5-turbo", - messages=[ - { - "role": "system", - "content": self.system_content, - }, - { - "role": "user", - "content": content, - }, - ], - ) - t_text = ( - completion["choices"][0] - .get("message") - .get("content") - .encode("utf8") - .decode() - ) - print("=================================================") - if int(completion["usage"]["total_tokens"]) > self.max_num_token: - self.max_num_token = int(completion["usage"]["total_tokens"]) - print( - f"The current largest total number of tokens update: {self.max_num_token}" + + completion = {} + try: + completion = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[ + { + "role": "system", + "content": self.system_content, + }, + { + "role": "user", + "content": content, + }, + ], ) + except Exception: + if completion["choices"][0]["finish_reason"] != "length": + raise + + choice = completion["choices"][0] + + t_text = choice.get("message").get("content").encode("utf8").decode() - # self.max_num_token = max( - # self.max_num_token, int(completion["usage"]["total_tokens"]) - # ) - # print( - # f"{completion['usage']['total_tokens']} {completion['usage']['prompt_tokens']} {completion['usage']['completion_tokens']} {self.max_num_token} (total_token, prompt_token, completion_tokens, max_history_total_token)" - # ) + if choice["finish_reason"] == "length": + with open("long_text.txt", "a") as f: + print( + f"""================================================== +The total token is too long and cannot be completely translated\n +{text} +""", + file=f, + ) + + usage = completion["usage"] + if int(usage["total_tokens"]) > self.max_num_token: + self.max_num_token = int(usage["total_tokens"]) + print("=================================================") + print( + f"{usage['total_tokens']} {usage['prompt_tokens']} {usage['completion_tokens']} {self.max_num_token} (total_token, prompt_token, completion_tokens, max_history_total_token)" + ) return t_text def translate(self, text, needprint=True): From 8174ebe3dfb804360c7580aeaffc07dd9b41a05f Mon Sep 17 00:00:00 2001 From: h Date: Mon, 13 Mar 2023 15:04:19 +0800 Subject: [PATCH 13/27] shorter prompt --- book_maker/translator/chatgptapi_translator.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index 947da8e3..33902859 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -115,17 +115,14 @@ def translate_list(self, plist): new_str = new_str[: -len(sep)] plist_len = len(plist) - always_trans = """[If there are any links, images, figure, listing or other content that cannot be translated, please leave them in the original language. If you cannot translate the content, please include it in brackets like this]: -[Insert Original Content Here] - """ - self.system_content += f"""Please translate the following paragraphs individually while preserving their original structure(This time it should be exactly {plist_len} paragraphs, no more or less). {always_trans}. Only translate the paragraphs provided below: + self.system_content = f"""{environ.get("OPENAI_API_SYS_MSG") or ""}. Please translate the following paragraphs individually while preserving their original structure, do not untranslate or merge any paragraphs, Only translate the paragraphs provided below: -[Insert first paragraph here] +[Insert first paragraph] -[Insert second paragraph here] +[Insert next paragraph] -[Insert third paragraph here] +[Insert next paragraph] """ retry_count = 0 From b424b3f5b433a21e8ca1275bc7805b8002604d4f Mon Sep 17 00:00:00 2001 From: h Date: Mon, 13 Mar 2023 15:04:49 +0800 Subject: [PATCH 14/27] Cumulative tokens instead of characters --- book_maker/cli.py | 2 +- book_maker/loader/epub_loader.py | 38 ++++++++++++++++++- .../translator/chatgptapi_translator.py | 5 ++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/book_maker/cli.py b/book_maker/cli.py index 952b66ee..cf1cda9f 100644 --- a/book_maker/cli.py +++ b/book_maker/cli.py @@ -134,7 +134,7 @@ def main(): dest="accumulated_num", type=int, default=1, - help="Wait for how many characters have been accumulated before starting the translation", + help="Wait for how many tokens have been accumulated before starting the translation", ) options = parser.parse_args() diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index d50b8385..269cf825 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -1,6 +1,7 @@ import os import re import pickle +import tiktoken import sys from copy import copy from pathlib import Path @@ -13,6 +14,39 @@ from .base_loader import BaseBookLoader +# ref: https://platform.openai.com/docs/guides/chat/introduction +def num_tokens_from_text(text, model="gpt-3.5-turbo-0301"): + messages = ( + { + "role": "user", + "content": text, + }, + ) + + """Returns the number of tokens used by a list of messages.""" + try: + encoding = tiktoken.encoding_for_model(model) + except KeyError: + encoding = tiktoken.get_encoding("cl100k_base") + if model == "gpt-3.5-turbo-0301": # note: future models may deviate from this + num_tokens = 0 + for message in messages: + num_tokens += ( + 4 # every message follows {role/name}\n{content}\n + ) + for key, value in message.items(): + num_tokens += len(encoding.encode(value)) + if key == "name": # if there's a name, the role is omitted + num_tokens += -1 # role is always required and always 1 token + num_tokens += 2 # every reply is primed with assistant + return num_tokens + else: + raise NotImplementedError( + f"""num_tokens_from_messages() is not presently implemented for model {model}. + See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.""" + ) + + def isLink(text): url_pattern = re.compile( r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" @@ -164,7 +198,7 @@ def deal_old(wait_p_list): or isList(temp_p.text) ): continue - length = len(p.text) + length = num_tokens_from_text(temp_p.text) if length > send_num: deal_new(p, wait_p_list) continue @@ -181,7 +215,7 @@ def deal_old(wait_p_list): else: deal_old(wait_p_list) wait_p_list.append(p) - count = len(p.text) + count = length else: is_test_done = self.is_test and index > self.test_num for p in p_list: diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index 33902859..cbd0cb23 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -63,9 +63,10 @@ def get_translation(self, text): ) usage = completion["usage"] + print("=================================================") + print(f"total_token: {usage['total_tokens']}") if int(usage["total_tokens"]) > self.max_num_token: self.max_num_token = int(usage["total_tokens"]) - print("=================================================") print( f"{usage['total_tokens']} {usage['prompt_tokens']} {usage['completion_tokens']} {self.max_num_token} (total_token, prompt_token, completion_tokens, max_history_total_token)" ) @@ -153,7 +154,7 @@ def translate_list(self, plist): newlist = new_str.split(sep) with open("buglog.txt", "a") as f: print(f"problem size: {plist_len - len(result_list)}", file=f) - for i in range(0, len(newlist)): + for i in range(len(newlist)): print(newlist[i], file=f) print(file=f) if i < len(result_list): From b93fd4467a382e1551282837495c3755556ad419 Mon Sep 17 00:00:00 2001 From: h Date: Tue, 14 Mar 2023 03:54:46 +0800 Subject: [PATCH 15/27] reduce err --- book_maker/loader/epub_loader.py | 12 ++++++++++++ book_maker/translator/chatgptapi_translator.py | 18 ++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index 269cf825..85ecfaaa 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -54,6 +54,14 @@ def isLink(text): return bool(url_pattern.match(text.strip())) +def isTailLink(text, num=100): + text = text.strip() + url_pattern = re.compile( + r".*http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$" + ) + return bool(url_pattern.match(text)) and len(text) < num + + def isSource(text): return text.strip().startswith("Source: ") @@ -196,6 +204,7 @@ def deal_old(wait_p_list): or self._is_special_text(temp_p.text) or isSource(temp_p.text) or isList(temp_p.text) + or isTailLink(temp_p.text) ): continue length = num_tokens_from_text(temp_p.text) @@ -212,6 +221,9 @@ def deal_old(wait_p_list): if count + length < send_num: count += length wait_p_list.append(p) + if len(wait_p_list) > 15 and count > send_num / 2: + deal_old(wait_p_list) + count = 0 else: deal_old(wait_p_list) wait_p_list.append(p) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index cbd0cb23..fbb1ce72 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -73,6 +73,7 @@ def get_translation(self, text): return t_text def translate(self, text, needprint=True): + start_time = time.time() # todo: Determine whether to print according to the cli option if needprint: print(re.sub("\n{3,}", "\n\n", text)) @@ -93,6 +94,10 @@ def translate(self, text, needprint=True): # todo: Determine whether to print according to the cli option if needprint: print(re.sub("\n{3,}", "\n\n", t_text)) + + elapsed_time = time.time() - start_time + print(f"translation time: {elapsed_time:.1f}s") + return t_text def translate_and_split_lines(self, text): @@ -117,14 +122,19 @@ def translate_list(self, plist): plist_len = len(plist) - self.system_content = f"""{environ.get("OPENAI_API_SYS_MSG") or ""}. Please translate the following paragraphs individually while preserving their original structure, do not untranslate or merge any paragraphs, Only translate the paragraphs provided below: + self.system_content = f"""{environ.get("OPENAI_API_SYS_MSG") or ""} + +Please translate the following paragraphs individually while preserving their original structure(This time it should be exactly {plist_len} paragraphs, no more or less). +Only translate the paragraphs provided below: -[Insert first paragraph] +[Insert first paragraph here] -[Insert next paragraph] +[Insert second paragraph here] -[Insert next paragraph] +[Insert ... paragraph here] """ + # print(self.system_content) + print(f"plist len = {len(plist)}") retry_count = 0 sleep_dur = 6 From 8baf990275c08057262bd125bdd0c328a0ec4fb4 Mon Sep 17 00:00:00 2001 From: h Date: Tue, 14 Mar 2023 11:44:00 +0800 Subject: [PATCH 16/27] deal figure, change output for test --- book_maker/loader/epub_loader.py | 9 +++++++++ book_maker/translator/chatgptapi_translator.py | 8 ++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index 85ecfaaa..68e2f9de 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -71,6 +71,11 @@ def isList(text, num=80): return re.match(r"^Listing\s*\d+", text) and len(text) < num +def isFigure(text, num=80): + text = text.strip() + return re.match(r"^Figure\s*\d+", text) and len(text) < num + + class EPUBBookLoader(BaseBookLoader): def __init__( self, @@ -180,6 +185,9 @@ def deal_old(wait_p_list): new_book.add_item(item) for item in self.origin_book.get_items_of_type(ITEM_DOCUMENT): + with open("buglog.txt", "a") as f: + print(f"------------- {item.file_name} -------------", file=f) + # if item.file_name != "OEBPS/ch01.xhtml": # continue @@ -204,6 +212,7 @@ def deal_old(wait_p_list): or self._is_special_text(temp_p.text) or isSource(temp_p.text) or isList(temp_p.text) + or isFigure(temp_p.text) or isTailLink(temp_p.text) ): continue diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index fbb1ce72..0faf1a83 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -140,7 +140,10 @@ def translate_list(self, plist): sleep_dur = 6 result_list = self.translate_and_split_lines(new_str) - while len(result_list) != plist_len and retry_count < 5: + start_time = time.time() + end_time = time.time() + + while len(result_list) != plist_len and retry_count < 15: print( f"bug: {plist_len} -> {len(result_list)} : Number of paragraphs before and after translation" ) @@ -148,6 +151,7 @@ def translate_list(self, plist): time.sleep(sleep_dur) result_list = self.translate_and_split_lines(new_str) retry_count += 1 + end_time = time.time() state = "fail" if len(result_list) != plist_len else "success" @@ -155,7 +159,7 @@ def translate_list(self, plist): print(f"retry {state}") with open("buglog.txt", "a") as f: print( - f"retry {state}, count = {retry_count}", + f"retry {state}, count = {retry_count}, time = {(end_time-start_time):.1f}s", file=f, ) From e7b6c27fa416be91142915b6b3ad8e4b21d8dea2 Mon Sep 17 00:00:00 2001 From: h Date: Tue, 14 Mar 2023 13:27:22 +0800 Subject: [PATCH 17/27] If there will be errors in the end, choose the least erroneous --- book_maker/translator/chatgptapi_translator.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index 0faf1a83..a6c21986 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -143,6 +143,8 @@ def translate_list(self, plist): start_time = time.time() end_time = time.time() + best_result_list = result_list + while len(result_list) != plist_len and retry_count < 15: print( f"bug: {plist_len} -> {len(result_list)} : Number of paragraphs before and after translation" @@ -150,9 +152,23 @@ def translate_list(self, plist): print(f"sleep for {sleep_dur}s and retry {retry_count+1} ...") time.sleep(sleep_dur) result_list = self.translate_and_split_lines(new_str) + if ( + len(result_list) == plist_len + or ( + len(result_list) > len(best_result_list) + and len(result_list) <= plist_len + ) + or ( + len(result_list) < len(best_result_list) + and len(best_result_list) > plist_len + ) + ): + best_result_list = result_list retry_count += 1 end_time = time.time() + result_list = best_result_list + state = "fail" if len(result_list) != plist_len else "success" if retry_count > 0: From b325621ce9c7273b7531b1693f4ea323c73bbd6d Mon Sep 17 00:00:00 2001 From: h Date: Tue, 14 Mar 2023 19:35:04 +0800 Subject: [PATCH 18/27] revert write book --- book_maker/loader/epub_loader.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index fde2b0eb..3ba1bbb5 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -273,8 +273,11 @@ def deal_old(wait_p_list): item.content = soup.prettify().encode() new_book.add_item(item) - name, _ = os.path.splitext(self.epub_name) - epub.write_epub(f"{name}_bilingual.epub", new_book, {}) + if self.accumulated_num > 1: + name, _ = os.path.splitext(self.epub_name) + epub.write_epub(f"{name}_bilingual.epub", new_book, {}) + name, _ = os.path.splitext(self.epub_name) + epub.write_epub(f"{name}_bilingual.epub", new_book, {}) if self.accumulated_num == 1: pbar.close() except (KeyboardInterrupt, Exception) as e: From 226da389df8e1c76a2c345e389898535c1f31e96 Mon Sep 17 00:00:00 2001 From: h Date: Wed, 15 Mar 2023 11:48:26 +0800 Subject: [PATCH 19/27] clean --- .gitignore | 3 ++- book_maker/loader/epub_loader.py | 4 +++- book_maker/translator/chatgptapi_translator.py | 17 ++++++++++------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 8c708edf..37af731c 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,5 @@ dmypy.json # Pyre type checker .pyre/ -/test_books/*.epub \ No newline at end of file +/test_books/*.epub +log/ diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index 3ba1bbb5..427d86d5 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -195,6 +195,8 @@ def deal_old(wait_p_list): for item in self.origin_book.get_items_of_type(ITEM_DOCUMENT): # if item.file_name != "OEBPS/ch01.xhtml": # continue + if not os.path.exists("log"): + os.makedirs("log") soup = bs(item.content, "html.parser") p_list = soup.findAll(trans_taglist) @@ -203,7 +205,7 @@ def deal_old(wait_p_list): send_num = self.accumulated_num if send_num > 1: - with open("buglog.txt", "a") as f: + with open("log/buglog.txt", "a") as f: print(f"------------- {item.file_name} -------------", file=f) print("------------------------------------------------------") diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index 141b34ad..b5af3c4e 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -76,6 +76,10 @@ def get_translation(self, text): if completion["choices"][0]["finish_reason"] != "length": raise + if len(completion["choices"]) == 0: + print('len(completion["choices"]) == 0') + return 'len(completion["choices"]) == 0' + choice = completion["choices"][0] t_text = choice.get("message").get("content").encode("utf8").decode() @@ -113,6 +117,7 @@ def translate(self, text, needprint=True): # 1. openai server error or own network interruption, sleep for a fixed time # 2. an apikey has no money or reach limit, don’t sleep, just replace it with another apikey # 3. all apikey reach limit, then use current sleep + print(e) sleep_time = int(60 / self.key_len) print(e, f"will sleep {sleep_time} seconds") time.sleep(sleep_time) @@ -182,10 +187,7 @@ def translate_list(self, plist): result_list = self.translate_and_split_lines(new_str) if ( len(result_list) == plist_len - or ( - len(result_list) > len(best_result_list) - and len(result_list) <= plist_len - ) + or len(best_result_list) < len(result_list) <= plist_len or ( len(result_list) < len(best_result_list) and len(best_result_list) > plist_len @@ -199,18 +201,19 @@ def translate_list(self, plist): state = "fail" if len(result_list) != plist_len else "success" + log_path = "log/buglog.txt" + if retry_count > 0: print(f"retry {state}") - with open("buglog.txt", "a") as f: + with open(log_path, "a") as f: print( f"retry {state}, count = {retry_count}, time = {(end_time-start_time):.1f}s", file=f, ) if len(result_list) != plist_len: - # todo: select best newlist = new_str.split(sep) - with open("buglog.txt", "a") as f: + with open(log_path, "a") as f: print(f"problem size: {plist_len - len(result_list)}", file=f) for i in range(len(newlist)): print(newlist[i], file=f) From 6e714d33791f290b5b8206c1e1facaa4ad216d03 Mon Sep 17 00:00:00 2001 From: h Date: Wed, 15 Mar 2023 16:34:03 +0800 Subject: [PATCH 20/27] refactor epub_loader by gpt4 refactor: _process_paragraph refactor: translate_paragraphs_acc --- book_maker/loader/epub_loader.py | 172 +++++++++++++++++-------------- 1 file changed, 96 insertions(+), 76 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index 427d86d5..d1922a01 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -17,6 +17,33 @@ from .base_loader import BaseBookLoader +class EPUBBookLoaderHelper: + def __init__(self, translate_model, accumulated_num): + self.translate_model = translate_model + self.accumulated_num = accumulated_num + + def deal_new(self, p, wait_p_list): + self.deal_old(wait_p_list) + new_p = copy(p) + new_p.string = self.translate_model.translate(p.text) + p.insert_after(new_p) + + def deal_old(self, wait_p_list): + if len(wait_p_list) == 0: + return + + result_txt_list = self.translate_model.translate_list(wait_p_list) + + for i in range(len(wait_p_list)): + if i < len(result_txt_list): + p = wait_p_list[i] + new_p = copy(p) + new_p.string = result_txt_list[i] + p.insert_after(new_p) + + wait_p_list.clear() + + # ref: https://platform.openai.com/docs/guides/chat/introduction def num_tokens_from_text(text, model="gpt-3.5-turbo-0301"): messages = ( @@ -110,6 +137,7 @@ def __init__( self.translate_tags = translate_tags self.allow_navigable_strings = allow_navigable_strings self.accumulated_num = accumulated_num + self.helper = EPUBBookLoaderHelper(self.translate_model, self.accumulated_num) try: self.origin_book = epub.read_epub(self.epub_name) @@ -146,28 +174,71 @@ def _make_new_book(self, book): new_book.toc = book.toc return new_book - def make_bilingual_book(self): - def deal_new(p, wait_p_list): - deal_old(wait_p_list) - new_p = copy(p) - new_p.string = self.translate_model.translate(p.text) - p.insert_after(new_p) - - def deal_old(wait_p_list): - if len(wait_p_list) == 0: - return - - result_txt_list = self.translate_model.translate_list(wait_p_list) - - for i in range(len(wait_p_list)): - if i < len(result_txt_list): - p = wait_p_list[i] - new_p = copy(p) - new_p.string = result_txt_list[i] - p.insert_after(new_p) - - wait_p_list.clear() + def _process_paragraph(self, p, index, p_to_save_len): + if not p.text or self._is_special_text(p.text): + return index + + new_p = copy(p) + + if self.resume and index < p_to_save_len: + new_p.string = self.p_to_save[index] + else: + if type(p) == NavigableString: + new_p = self.translate_model.translate(p.text) + self.p_to_save.append(new_p) + else: + new_p.string = self.translate_model.translate(p.text) + self.p_to_save.append(new_p.text) + + p.insert_after(new_p) + index += 1 + + if index % 20 == 0: + self._save_progress() + + return index + + def translate_paragraphs_acc(self, p_list, send_num): + count = 0 + wait_p_list = [] + for i in range(len(p_list)): + p = p_list[i] + temp_p = copy(p) + for sup in temp_p.find_all("sup"): + sup.extract() + if ( + not p.text + or self._is_special_text(temp_p.text) + or is_source(temp_p.text) + or is_list(temp_p.text) + or is_figure(temp_p.text) + or is_tail_Link(temp_p.text) + ): + continue + length = num_tokens_from_text(temp_p.text) + if length > send_num: + self.helper.deal_new(p, wait_p_list) + continue + if i == len(p_list) - 1: + if count + length < send_num: + wait_p_list.append(p) + self.helper.deal_old(wait_p_list) + else: + self.helper.deal_new(p, wait_p_list) + break + if count + length < send_num: + count += length + wait_p_list.append(p) + # This is because the more paragraphs, the easier it is possible to translate different numbers of paragraphs, maybe you should find better values than 15 and 2 + if len(wait_p_list) > 15 and count > send_num / 2: + self.helper.deal_old(wait_p_list) + count = 0 + else: + self.helper.deal_old(wait_p_list) + wait_p_list.append(p) + count = length + def make_bilingual_book(self): new_book = self._make_new_book(self.origin_book) all_items = list(self.origin_book.get_items()) trans_taglist = self.translate_tags.split(",") @@ -210,64 +281,13 @@ def deal_old(wait_p_list): print("------------------------------------------------------") print(f"dealing {item.file_name} ...") - count = 0 - wait_p_list = [] - for i in range(len(p_list)): - p = p_list[i] - temp_p = copy(p) - for sup in temp_p.find_all("sup"): - sup.extract() - if ( - not p.text - or self._is_special_text(temp_p.text) - or is_source(temp_p.text) - or is_list(temp_p.text) - or is_figure(temp_p.text) - or is_tail_Link(temp_p.text) - ): - continue - length = num_tokens_from_text(temp_p.text) - if length > send_num: - deal_new(p, wait_p_list) - continue - if i == len(p_list) - 1: - if count + length < send_num: - wait_p_list.append(p) - deal_old(wait_p_list) - else: - deal_new(p, wait_p_list) - break - if count + length < send_num: - count += length - wait_p_list.append(p) - if len(wait_p_list) > 15 and count > send_num / 2: - deal_old(wait_p_list) - count = 0 - else: - deal_old(wait_p_list) - wait_p_list.append(p) - count = length + self.translate_paragraphs_acc(p_list, send_num) else: is_test_done = self.is_test and index > self.test_num for p in p_list: - if is_test_done or not p.text or self._is_special_text(p.text): - continue - new_p = copy(p) - # TODO banch of p to translate then combine - # PR welcome here - if self.resume and index < p_to_save_len: - new_p.string = self.p_to_save[index] - else: - if type(p) == NavigableString: - new_p = self.translate_model.translate(p.text) - self.p_to_save.append(new_p) - else: - new_p.string = self.translate_model.translate(p.text) - self.p_to_save.append(new_p.text) - p.insert_after(new_p) - index += 1 - if index % 20 == 0: - self._save_progress() + if is_test_done: + break + index = self._process_paragraph(p, index, p_to_save_len) # pbar.update(delta) not pbar.update(index)? pbar.update(1) if self.is_test and index >= self.test_num: From 4e4286067212d52e8e6f647fe8f24798a60339ce Mon Sep 17 00:00:00 2001 From: h Date: Wed, 15 Mar 2023 18:54:41 +0800 Subject: [PATCH 21/27] refactor --- .../translator/chatgptapi_translator.py | 147 ++++++++++-------- 1 file changed, 82 insertions(+), 65 deletions(-) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index b5af3c4e..df67eb19 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -49,29 +49,27 @@ def __init__( def rotate_key(self): openai.api_key = next(self.keys) - def get_translation(self, text): - self.rotate_key() + def create_chat_completion(self, text): content = self.prompt_template.format(text=text, language=self.language) sys_content = self.prompt_sys_msg if self.system_content: sys_content = self.system_content - messages = [] - messages.append( + messages = [ {"role": "system", "content": sys_content}, + {"role": "user", "content": content}, + ] + + return openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=messages, ) - messages.append( - { - "role": "user", - "content": content, - } - ) + + def get_translation(self, text): + self.rotate_key() completion = {} try: - completion = openai.ChatCompletion.create( - model="gpt-3.5-turbo", - messages=messages, - ) + completion = self.create_chat_completion(text) except Exception: if completion["choices"][0]["finish_reason"] != "length": raise @@ -117,7 +115,6 @@ def translate(self, text, needprint=True): # 1. openai server error or own network interruption, sleep for a fixed time # 2. an apikey has no money or reach limit, don’t sleep, just replace it with another apikey # 3. all apikey reach limit, then use current sleep - print(e) sleep_time = int(60 / self.key_len) print(e, f"will sleep {sleep_time} seconds") time.sleep(sleep_time) @@ -139,6 +136,70 @@ def translate_and_split_lines(self, text): lines = [line.strip() for line in lines if line.strip() != ""] return lines + def get_best_result_list( + self, plist_len, new_str, sleep_dur, result_list, max_retries=15 + ): + if len(result_list) == plist_len: + return result_list, 0 + + best_result_list = result_list + retry_count = 0 + + while retry_count < max_retries: + print( + f"bug: {plist_len} -> {len(result_list)} : Number of paragraphs before and after translation" + ) + print(f"sleep for {sleep_dur}s and retry {retry_count+1} ...") + time.sleep(sleep_dur) + retry_count += 1 + result_list = self.translate_and_split_lines(new_str) + if ( + len(result_list) == plist_len + or len(best_result_list) < len(result_list) <= plist_len + or ( + len(result_list) < len(best_result_list) + and len(best_result_list) > plist_len + ) + ): + best_result_list = result_list + + if len(result_list) == plist_len: + break + + return best_result_list, retry_count + + def log_retry(self, state, retry_count, elapsed_time, log_path="log/buglog.txt"): + if retry_count == 0: + return + print(f"retry {state}") + with open(log_path, "a") as f: + print( + f"retry {state}, count = {retry_count}, time = {elapsed_time:.1f}s", + file=f, + ) + + def log_translation_mismatch( + self, plist_len, result_list, new_str, sep, log_path="log/buglog.txt" + ): + if len(result_list) != plist_len: + return + newlist = new_str.split(sep) + with open(log_path, "a") as f: + print(f"problem size: {plist_len - len(result_list)}", file=f) + for i in range(len(newlist)): + print(newlist[i], file=f) + print(file=f) + if i < len(result_list): + print(result_list[i], file=f) + print(file=f) + print("=============================", file=f) + print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") + + print( + f"bug: {plist_len} paragraphs of text translated into {len(result_list)} paragraphs" + ) + print("continue") + def translate_list(self, plist): sep = "\n\n\n\n\n" # new_str = sep.join([item.text for item in plist]) @@ -169,64 +230,20 @@ def translate_list(self, plist): # print(self.system_content) print(f"plist len = {len(plist)}") - retry_count = 0 - sleep_dur = 6 result_list = self.translate_and_split_lines(new_str) start_time = time.time() - end_time = time.time() - - best_result_list = result_list - while len(result_list) != plist_len and retry_count < 15: - print( - f"bug: {plist_len} -> {len(result_list)} : Number of paragraphs before and after translation" - ) - print(f"sleep for {sleep_dur}s and retry {retry_count+1} ...") - time.sleep(sleep_dur) - result_list = self.translate_and_split_lines(new_str) - if ( - len(result_list) == plist_len - or len(best_result_list) < len(result_list) <= plist_len - or ( - len(result_list) < len(best_result_list) - and len(best_result_list) > plist_len - ) - ): - best_result_list = result_list - retry_count += 1 - end_time = time.time() + result_list, retry_count = self.get_best_result_list( + plist_len, new_str, 6, result_list + ) - result_list = best_result_list + end_time = time.time() state = "fail" if len(result_list) != plist_len else "success" - log_path = "log/buglog.txt" - if retry_count > 0: - print(f"retry {state}") - with open(log_path, "a") as f: - print( - f"retry {state}, count = {retry_count}, time = {(end_time-start_time):.1f}s", - file=f, - ) - - if len(result_list) != plist_len: - newlist = new_str.split(sep) - with open(log_path, "a") as f: - print(f"problem size: {plist_len - len(result_list)}", file=f) - for i in range(len(newlist)): - print(newlist[i], file=f) - print(file=f) - if i < len(result_list): - print(result_list[i], file=f) - print(file=f) - print("=============================", file=f) - print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") - - print( - f"bug: {plist_len} paragraphs of text translated into {len(result_list)} paragraphs" - ) - print("continue") + self.log_retry(state, retry_count, end_time - start_time, log_path) + self.log_translation_mismatch(plist_len, result_list, new_str, sep, log_path) return result_list From cc85d6bf161bf3161b653da7dbc9dd92a9f74630 Mon Sep 17 00:00:00 2001 From: h Date: Wed, 15 Mar 2023 21:09:29 +0800 Subject: [PATCH 22/27] update readme and help --- README.md | 15 +++++++++------ book_maker/cli.py | 7 ++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 26f01c6b..71647232 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ The bilingual_book_maker is an AI translation tool that uses ChatGPT to assist u ## Use - `pip install -r requirements.txt` or `pip install -U bbook_maker`(you can use) -- Use `--openai_key` option to specify OpenAI API key. If you have multiple keys, separate them by commas (xxx,xxx,xxx) to reduce errors caused by API call limits. +- Use `--openai_key` option to specify OpenAI API key. If you have multiple keys, separate them by commas (xxx,xxx,xxx) to reduce errors caused by API call limits. Or, just set environment variable `BMM_OPENAI_API_KEY` instead. - A sample book, `test_books/animal_farm.epub`, is provided for testing purposes. - The default underlying model is [GPT-3.5-turbo](https://openai.com/blog/introducing-chatgpt-and-whisper-apis), which is used by ChatGPT currently. Use `--model gpt3` to change the underlying model to `GPT3` 5. support DeepL model [DeepL Translator](https://rapidapi.com/splintPRO/api/deepl-translator) need pay to get the token use `--model deepl --deepl_key ${deepl_key}` - Use `--test` option to preview the result if you haven't paid for the service. Note that there is a limit and it may take some time. -- Set the target language like `--language "Simplified Chinese"`. Default target language is `"Simplified Chinese"`. +- Set the target language like `--language "Simplified Chinese"`. Default target language is `"Simplified Chinese"`. Read available languages by helper message: `python make_book.py --help` - Use `--proxy` option to specify proxy server for internet access. Enter a string such as `http://127.0.0.1:7890`. - Use `--resume` option to manually resume the process after an interruption. @@ -30,19 +30,22 @@ The bilingual_book_maker is an AI translation tool that uses ChatGPT to assist u Use `--translate-tags` to specify tags need for translation. Use comma to seperate multiple tags. For example: `--translate-tags h1,h2,h3,p,div` - Use `--book_from` option to specify e-reader type (Now only `kobo` is available), and use `--device_path` to specify the mounting point. -- If you want to change api_base like using Cloudflare Workers, use `--api_base ` to support it. +- If you want to change api_base like using Cloudflare Workers, use `--api_base ` to support it. **Note: the api url should be '`https://xxxx/v1`'. Quotation marks are required.** - Once the translation is complete, a bilingual book named `${book_name}_bilingual.epub` would be generated. - If there are any errors or you wish to interrupt the translation by pressing `CTRL+C`. A book named `${book_name}_bilingual_temp.epub` would be generated. You can simply rename it to any desired name. - If you want to translate strings in an e-book that aren't labeled with any tags, you can use the `--allow_navigable_strings` parameter. This will add the strings to the translation queue. **Note that it's best to look for e-books that are more standardized if possible.** -- To tweak the prompt, use the `--prompt` parameter. Valid placeholders for the `user` role template include `{text}` and `{language}`. It supports a few ways to configure the prompt: - If you don't need to set the `system` role content, you can simply set it up like this: `--prompt "Translate {text} to {language}."` or `--prompt prompt_template_sample.txt` (example of a text file can be found at [./prompt_template_sample.txt](./prompt_template_sample.txt)). - If you need to set the `system` role content, you can use the following format: `--prompt '{"user":"Translate {text} to {language}", "system": "You are a professional translator."}'` or `--prompt prompt_template_sample.json` (example of a JSON file can be found at [./prompt_template_sample.json](./prompt_template_sample.json)). +- To tweak the prompt, use the `--prompt` parameter. Valid placeholders for the `user` role template include `{text}` and `{language}`. It supports a few ways to configure the prompt: + If you don't need to set the `system` role content, you can simply set it up like this: `--prompt "Translate {text} to {language}."` or `--prompt prompt_template_sample.txt` (example of a text file can be found at [./prompt_template_sample.txt](./prompt_template_sample.txt)). + If you need to set the `system` role content, you can use the following format: `--prompt '{"user":"Translate {text} to {language}", "system": "You are a professional translator."}'` or `--prompt prompt_template_sample.json` (example of a JSON file can be found at [./prompt_template_sample.json](./prompt_template_sample.json)). You can also set the `user` and `system` role prompt by setting environment variables: `BBM_CHATGPTAPI_USER_MSG_TEMPLATE` and `BBM_CHATGPTAPI_SYS_MSG`. - Once the translation is complete, a bilingual book named `${book_name}_bilingual.epub` would be generated. - If there are any errors or you wish to interrupt the translation by pressing `CTRL+C`. A book named `${book_name}_bilingual_temp.epub` would be generated. You can simply rename it to any desired name. - If you want to translate strings in an e-book that aren't labeled with any tags, you can use the `--allow_navigable_strings` parameter. This will add the strings to the translation queue. **Note that it's best to look for e-books that are more standardized if possible.** - Use the `--batch_size` parameter to specify the number of lines for batch translation (default is 10, currently only effective for txt files). +- `--accumulated_num` Wait for how many tokens have been accumulated before starting the translation. gpt3.5 limits the total_token to 4090. For example, if you use --accumulated_num 1600, maybe openai will +output 2200 tokens and maybe 200 tokens for other messages in the system messages user messages, 1600+2200+200=4000, So you are close to reaching the limit. You have to choose your own +value, there is no way to know if the limit is reached before sending ### Examples diff --git a/book_maker/cli.py b/book_maker/cli.py index 8b0358f5..66c1f5c2 100644 --- a/book_maker/cli.py +++ b/book_maker/cli.py @@ -175,7 +175,12 @@ def main(): dest="accumulated_num", type=int, default=1, - help="Wait for how many tokens have been accumulated before starting the translation", + help="""Wait for how many tokens have been accumulated before starting the translation. +gpt3.5 limits the total_token to 4090. +For example, if you use --accumulated_num 1600, maybe openai will output 2200 tokens +and maybe 200 tokens for other messages in the system messages user messages, 1600+2200+200=4000, +So you are close to reaching the limit. You have to choose your own value, there is no way to know if the limit is reached before sending +""", ) parser.add_argument( "--batch_size", From c859c69d100ca0219a515aaabf68d9c7c8f96f1e Mon Sep 17 00:00:00 2001 From: h Date: Wed, 15 Mar 2023 21:20:38 +0800 Subject: [PATCH 23/27] fix --- book_maker/translator/chatgptapi_translator.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index df67eb19..8ac2da67 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -145,7 +145,7 @@ def get_best_result_list( best_result_list = result_list retry_count = 0 - while retry_count < max_retries: + while retry_count < max_retries and len(result_list) != plist_len: print( f"bug: {plist_len} -> {len(result_list)} : Number of paragraphs before and after translation" ) @@ -163,9 +163,6 @@ def get_best_result_list( ): best_result_list = result_list - if len(result_list) == plist_len: - break - return best_result_list, retry_count def log_retry(self, state, retry_count, elapsed_time, log_path="log/buglog.txt"): @@ -181,7 +178,7 @@ def log_retry(self, state, retry_count, elapsed_time, log_path="log/buglog.txt") def log_translation_mismatch( self, plist_len, result_list, new_str, sep, log_path="log/buglog.txt" ): - if len(result_list) != plist_len: + if len(result_list) == plist_len: return newlist = new_str.split(sep) with open(log_path, "a") as f: From 173b7564fff9ecfc8af354b92c8b735490fc422e Mon Sep 17 00:00:00 2001 From: h Date: Wed, 15 Mar 2023 22:06:28 +0800 Subject: [PATCH 24/27] improve exception --- .../translator/chatgptapi_translator.py | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index 8ac2da67..ce64489b 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -71,13 +71,16 @@ def get_translation(self, text): try: completion = self.create_chat_completion(text) except Exception: + if ( + not "choices" in completion + or not isinstance(completion["choices"], list) + or len(completion["choices"]) == 0 + ): + raise if completion["choices"][0]["finish_reason"] != "length": raise - if len(completion["choices"]) == 0: - print('len(completion["choices"]) == 0') - return 'len(completion["choices"]) == 0' - + # work well or exception finish by length limit choice = completion["choices"][0] t_text = choice.get("message").get("content").encode("utf8").decode() @@ -108,18 +111,26 @@ def translate(self, text, needprint=True): if needprint: print(re.sub("\n{3,}", "\n\n", text)) - try: - t_text = self.get_translation(text) - except Exception as e: - # todo: better sleep time? why sleep alawys about key_len - # 1. openai server error or own network interruption, sleep for a fixed time - # 2. an apikey has no money or reach limit, don’t sleep, just replace it with another apikey - # 3. all apikey reach limit, then use current sleep - sleep_time = int(60 / self.key_len) - print(e, f"will sleep {sleep_time} seconds") - time.sleep(sleep_time) - - t_text = self.get_translation(text) + attempt_count = 0 + max_attempts = 3 + t_text = "" + + while attempt_count < max_attempts: + try: + t_text = self.get_translation(text) + break + except Exception as e: + # todo: better sleep time? why sleep alawys about key_len + # 1. openai server error or own network interruption, sleep for a fixed time + # 2. an apikey has no money or reach limit, don’t sleep, just replace it with another apikey + # 3. all apikey reach limit, then use current sleep + sleep_time = int(60 / self.key_len) + print(e, f"will sleep {sleep_time} seconds") + time.sleep(sleep_time) + attempt_count += 1 + if attempt_count == max_attempts: + print(f"Get {attempt_count} consecutive exceptions") + raise # todo: Determine whether to print according to the cli option if needprint: From e24a1c54a1662a5912d97d5ab7e46718f709a18c Mon Sep 17 00:00:00 2001 From: h Date: Wed, 15 Mar 2023 22:06:28 +0800 Subject: [PATCH 25/27] improve exception --- .../translator/chatgptapi_translator.py | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index 8ac2da67..ce64489b 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -71,13 +71,16 @@ def get_translation(self, text): try: completion = self.create_chat_completion(text) except Exception: + if ( + not "choices" in completion + or not isinstance(completion["choices"], list) + or len(completion["choices"]) == 0 + ): + raise if completion["choices"][0]["finish_reason"] != "length": raise - if len(completion["choices"]) == 0: - print('len(completion["choices"]) == 0') - return 'len(completion["choices"]) == 0' - + # work well or exception finish by length limit choice = completion["choices"][0] t_text = choice.get("message").get("content").encode("utf8").decode() @@ -108,18 +111,26 @@ def translate(self, text, needprint=True): if needprint: print(re.sub("\n{3,}", "\n\n", text)) - try: - t_text = self.get_translation(text) - except Exception as e: - # todo: better sleep time? why sleep alawys about key_len - # 1. openai server error or own network interruption, sleep for a fixed time - # 2. an apikey has no money or reach limit, don’t sleep, just replace it with another apikey - # 3. all apikey reach limit, then use current sleep - sleep_time = int(60 / self.key_len) - print(e, f"will sleep {sleep_time} seconds") - time.sleep(sleep_time) - - t_text = self.get_translation(text) + attempt_count = 0 + max_attempts = 3 + t_text = "" + + while attempt_count < max_attempts: + try: + t_text = self.get_translation(text) + break + except Exception as e: + # todo: better sleep time? why sleep alawys about key_len + # 1. openai server error or own network interruption, sleep for a fixed time + # 2. an apikey has no money or reach limit, don’t sleep, just replace it with another apikey + # 3. all apikey reach limit, then use current sleep + sleep_time = int(60 / self.key_len) + print(e, f"will sleep {sleep_time} seconds") + time.sleep(sleep_time) + attempt_count += 1 + if attempt_count == max_attempts: + print(f"Get {attempt_count} consecutive exceptions") + raise # todo: Determine whether to print according to the cli option if needprint: From c5350412785db2f89e91d6b0fc2d8c0eea44221e Mon Sep 17 00:00:00 2001 From: h Date: Thu, 16 Mar 2023 18:24:53 +0800 Subject: [PATCH 26/27] use ordinals to ensure order instead of prompts --- book_maker/loader/epub_loader.py | 6 +++--- book_maker/translator/chatgptapi_translator.py | 18 +++++------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/book_maker/loader/epub_loader.py b/book_maker/loader/epub_loader.py index d1922a01..3a44e350 100644 --- a/book_maker/loader/epub_loader.py +++ b/book_maker/loader/epub_loader.py @@ -230,9 +230,9 @@ def translate_paragraphs_acc(self, p_list, send_num): count += length wait_p_list.append(p) # This is because the more paragraphs, the easier it is possible to translate different numbers of paragraphs, maybe you should find better values than 15 and 2 - if len(wait_p_list) > 15 and count > send_num / 2: - self.helper.deal_old(wait_p_list) - count = 0 + # if len(wait_p_list) > 15 and count > send_num / 2: + # self.helper.deal_old(wait_p_list) + # count = 0 else: self.helper.deal_old(wait_p_list) wait_p_list.append(p) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index ce64489b..a47aacbe 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -213,29 +213,19 @@ def translate_list(self, plist): # new_str = sep.join([item.text for item in plist]) new_str = "" + i = 1 for p in plist: temp_p = copy(p) for sup in temp_p.find_all("sup"): sup.extract() - new_str += temp_p.get_text().strip() + sep + new_str += f"({i}) " + temp_p.get_text().strip() + sep + i = i + 1 if new_str.endswith(sep): new_str = new_str[: -len(sep)] plist_len = len(plist) - self.system_content = f"""{environ.get("OPENAI_API_SYS_MSG") or ""} - -Please translate the following paragraphs individually while preserving their original structure(This time it should be exactly {plist_len} paragraphs, no more or less). -Only translate the paragraphs provided below: - -[Insert first paragraph here] - -[Insert second paragraph here] - -[Insert ... paragraph here] -""" - # print(self.system_content) print(f"plist len = {len(plist)}") result_list = self.translate_and_split_lines(new_str) @@ -254,4 +244,6 @@ def translate_list(self, plist): self.log_retry(state, retry_count, end_time - start_time, log_path) self.log_translation_mismatch(plist_len, result_list, new_str, sep, log_path) + # del (num), num. sometime (num) will translated to num. + result_list = [re.sub(r"^(\(\d+\)|\d+\.)\s*", "", s) for s in result_list] return result_list From b73240ab48d42e3f2ed763c2a207d13c17de4889 Mon Sep 17 00:00:00 2001 From: h Date: Thu, 16 Mar 2023 21:23:35 +0800 Subject: [PATCH 27/27] comment debug output for merge --- book_maker/translator/chatgptapi_translator.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/book_maker/translator/chatgptapi_translator.py b/book_maker/translator/chatgptapi_translator.py index a47aacbe..c0e1625c 100644 --- a/book_maker/translator/chatgptapi_translator.py +++ b/book_maker/translator/chatgptapi_translator.py @@ -95,17 +95,17 @@ def get_translation(self, text): file=f, ) - usage = completion["usage"] - print(f"total_token: {usage['total_tokens']}") - if int(usage["total_tokens"]) > self.max_num_token: - self.max_num_token = int(usage["total_tokens"]) - print( - f"{usage['total_tokens']} {usage['prompt_tokens']} {usage['completion_tokens']} {self.max_num_token} (total_token, prompt_token, completion_tokens, max_history_total_token)" - ) + # usage = completion["usage"] + # print(f"total_token: {usage['total_tokens']}") + # if int(usage["total_tokens"]) > self.max_num_token: + # self.max_num_token = int(usage["total_tokens"]) + # print( + # f"{usage['total_tokens']} {usage['prompt_tokens']} {usage['completion_tokens']} {self.max_num_token} (total_token, prompt_token, completion_tokens, max_history_total_token)" + # ) return t_text def translate(self, text, needprint=True): - print("=================================================") + # print("=================================================") start_time = time.time() # todo: Determine whether to print according to the cli option if needprint: @@ -137,7 +137,7 @@ def translate(self, text, needprint=True): print(re.sub("\n{3,}", "\n\n", t_text)) elapsed_time = time.time() - start_time - print(f"translation time: {elapsed_time:.1f}s") + # print(f"translation time: {elapsed_time:.1f}s") return t_text