├── .gitignore ├── README.md ├── chatgpt_automation ├── __init__.py ├── chatgpt_client.py ├── helpers.py └── talking_heads.py ├── configs.py ├── cookie_example.json ├── main.py ├── proofread.py ├── requirements.txt ├── text └── 01_01.json └── translator.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | chatgpt_automation/__pycache__/ 3 | test.py 4 | cookies.json 5 | .vscode/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VN-ChatGPT-Translator 2 | 3 | 一款基于python,使用ChatGPT网页版的视觉小说翻译器,具有以下特点: 4 | - 网页版自动输入文本,**无需api开销**。 5 | - ChatGPT4和ChatGPT3.5均可使用。 6 | - 多账号并发翻译,速度加倍。 7 | - 同一个session内翻译,使得ChatGPT**掌握上下文**,翻译更加精准。 8 | - 使用cookie(推荐)和undetected-chrome登录,方便快捷避免封号。 9 | - 同样支持账号密码登录。 10 | - 自动等待GPT4使用上限,即达到上限后会等待一段时间后自动重启翻译。 11 | 12 | ## 注意事项 13 | 14 | 这个项目可能有很多的坑点!由于本人没有时间维护,并且chatgpt是一个会不断更新的网页,所以很可能会出现用不了的情况。只建议有做过python爬虫经验的伙伴使用,如果用不了可以试着排查原因然后修改代码。祝好运。 15 | 16 | ## 安装 17 | 18 | ``` cmd 19 | pip install -r requirements.txt 20 | ``` 21 | 22 | ## 项目概述 23 | 24 | 本项目使用selenium操作ChatGPT网页,使用账号密码或注入cookie以实现登录。登录完成后,会将待翻译的文本以`batch_size`变量指定的句子数量为一批,发送给ChatGPT让其翻译。 25 | 26 | ## 使用方法 27 | 28 | 本段将以翻译樱之刻为例子讲解使用方法 29 | 30 | ### 放置待翻译文本 31 | 32 | 把待翻译文本放在`./text/`文件夹下,待翻译的对话需要为以下JSON格式: 33 | 34 | ``` json 35 | [{"静流":"「いらっしゃい」"}, 36 | {"???":"「……」"}, 37 | {"旁白":"一人の男性客が店に入るなり黙ってその場で立ち止まる。"}, 38 | {"静流":"「ん? どうしました?」"}, 39 | {"???":"「あ、いや、キマイラの店主さんがおかえりだって聞いていたので……」"}, 40 | {"静流":"「あれ? 君は、あれかな? 私がいない間のキマイラの常連さん?」"}, 41 | {"???":"「あ、まぁ、そんな感じですかね」"}, 42 | {"旁白":"ずいぶんとすらりとしたイケメンだ。"}, 43 | {"旁白":"初対面なのだが、どこかで会った事がある様な。"}, 44 | {"静流":"「そうですか。円木ちゃんが店長してた時からの常連さんですか?」"}] 45 | ``` 46 | 47 | 你可以参考本项目自带的`./text/01_01.json`中的对话格式。翻译后,生成的文本会储存在`./translated`文件夹下。 48 | 49 | ### 修改配置文件 50 | 51 | 以下是`config.py`中的内容 52 | 53 | ```python 54 | import os 55 | 56 | 57 | translated_file_dir = 'translated' 58 | text_dir = 'text' 59 | 60 | all_text = os.listdir(text_dir) 61 | 62 | total_accounts = 1 # 总共账号数量 63 | 64 | # 请在这里放置session的url。 65 | urls = { 66 | 1: 'https://chat.openai.com/c/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 67 | } 68 | 69 | # 请在这里放置cookies。 70 | cookies_mapping = {1: 'cookie_example.json'} 71 | 72 | prompt = 'please translate the following galgame conversations from Japanese to Chinese. ' + \ 73 | 'Please preserve escape characters like \\r\\n or \\" and strictly keep the JSON format of the provided content. ' + \ 74 | 'Please directly give the translated content only. ' +\ 75 | 'Here are the conversations:\n' 76 | 77 | # the time to wait for the answer from the chatbot. If the bot waits too short, error may occur. 78 | # 等待聊天机器人回答的时间。如果等待时间太短,可能会出现错误。 79 | answer_waiting_time = 10 80 | 81 | # the maximum number of lines to input to ChatGPT each time 82 | # 每次输入ChatGPT的最大行数 83 | batch_size = 15 84 | ``` 85 | 86 | 你可以按照如下步骤使脚本登录你的ChatGPT账号。 87 | 88 | **New Chat** 在ChatGPT网页中新建对话,刷新后将带有编号的浏览器顶部url复制到`urls`变量中。请确保格式为`https://chat.openai.com/c/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`。 89 | 90 | **获取cookie** 你可以在edge或chrome浏览器中安装`Cookie-Editor`扩展,并在登录ChatGPT之后将cookie导出(export)为json格式,粘贴进son文件中。之后,你需要将json文件名填写进`cookies_mapping`变量。 91 | 92 | **修改提示词(可选)** 把提示词改成更适合你的文本的提示词。 93 | 94 | **修改其他配置** 可以根据翻译效果修改。GPT4引擎可以使用15的`batch_size`,即一次输入15句话。GPT3引擎建议数量为10。 95 | 96 | ### 开始翻译 97 | 98 | 修改完配置文件后,你可以通过 99 | 100 | ``` 101 | python main.py 1 08:00:00 102 | ``` 103 | 104 | 使程序使用账号`1`在系统时间`08:00:00`后开始翻译。开始后,程序会根据账号数量把待翻译的文本分为若干等分,每个账号的翻译任务互不干扰。 105 | 106 | #### 多账号翻译说明 107 | 108 | 这个脚本的设计思路是根据账号数量把待翻译的文件分成若干等份。如果有两个账号,可以开两个控制台并分别输入`python main.py 1 00:00:00`和`python main.py 2 00:00:00`,这样它们就会分别翻译第1,3,5……个文件和第2,4,6……个文件。 109 | 110 | ### *使用账号密码登录(不推荐)* 111 | 112 | 本程序也可以使用账号密码登录,你可以通过修改`Translator`类中`ChatGPT_Client`的构造方式,在其中添加账号密码来实现登录。 113 | 114 | ### 使用ChatGPT自动校对 115 | 116 | `proofread.py`中提供了一些校对翻译是否出现重复、漏句等问题。部分文件翻译完成后,可以运行这个脚本检查是否有错漏。 117 | 118 | ## 致谢 119 | 120 | 本项目使用了[ChatGPT-Automation](https://github.com/ugorsahin/ChatGPT_Automation)为基础,并在此之上对原脚本修改使其适合连续翻译长对话。感谢这个脚本的作者。 121 | 122 | 关于对话的提取与json文件的制作,项目[VNTranslationTools](https://github.com/arcusmaximus/VNTranslationTools)帮了不少忙。如有想应用本项目做翻译的小伙伴,十分推荐这个项目,可以快速提取出对话文本。 123 | -------------------------------------------------------------------------------- /chatgpt_automation/__init__.py: -------------------------------------------------------------------------------- 1 | from .chatgpt_client import ChatGPT_Client 2 | from .talking_heads import TalkingHeads 3 | -------------------------------------------------------------------------------- /chatgpt_automation/chatgpt_client.py: -------------------------------------------------------------------------------- 1 | '''Class definition for ChatGPT_Client''' 2 | 3 | import logging 4 | import time 5 | import undetected_chromedriver as uc 6 | 7 | from selenium import webdriver 8 | from selenium.webdriver.common.by import By 9 | from selenium.webdriver.common.keys import Keys 10 | from selenium.webdriver.support.wait import WebDriverWait 11 | from selenium.webdriver.support import expected_conditions as EC 12 | import selenium.common.exceptions as Exceptions 13 | 14 | # from .helpers import detect_chrome_version 15 | 16 | logging.basicConfig( 17 | format='%(asctime)s %(levelname)s %(message)s', 18 | datefmt='%Y/%m/%d %H:%M:%S', 19 | level=logging.WARNING 20 | ) 21 | 22 | class ChatGPT_Client: 23 | '''ChatGPT_Client class to interact with ChatGPT''' 24 | 25 | login_xq = '//button[//div[text()="Log in"]]' 26 | continue_xq = '//button[text()="Continue"]' 27 | next_cq = 'prose' 28 | button_tq = 'button' 29 | # next_xq = '//button[//div[text()='Next']]' 30 | done_xq = '//button[//div[text()="Done"]]' 31 | # /html/body/div[3]/div/div/div/div[2]/div 32 | info_screen_xpath = '/html/body/div[3]/div/div/div/div[2]/div' 33 | 34 | chatbox_cq = 'text-base' 35 | wait_cq = 'text-2xl' 36 | reset_xq = '//a[text()="New chat"]' 37 | regen_xq = '//div[text()="Regenerate response"]' 38 | 39 | def __init__( 40 | self, 41 | cookie = None, 42 | url = None, 43 | username :str = None, 44 | password :str = None, 45 | use_gpt4: bool = False, 46 | headless :bool = True, 47 | cold_start :bool = False, 48 | driver_executable_path :str =None, 49 | driver_arguments : list = None, 50 | verbose :bool = True, 51 | answer_waiting_time :int = 10, 52 | chrome_version :int = 113 53 | ): 54 | self.answer_wating_time = answer_waiting_time 55 | if verbose: 56 | logging.getLogger().setLevel(logging.INFO) 57 | logging.info('Verbose mode active') 58 | options = uc.ChromeOptions() 59 | options.add_argument('--incognito') 60 | if headless: 61 | options.add_argument('--headless') 62 | if driver_arguments: 63 | for _arg in driver_arguments: 64 | options.add_argument(_arg) 65 | 66 | logging.info('Loading undetected Chrome') 67 | self.browser = uc.Chrome( 68 | driver_executable_path=driver_executable_path, 69 | options=options, 70 | headless=headless, 71 | version_main=chrome_version 72 | ) 73 | self.browser.set_page_load_timeout(15) 74 | logging.info('Loaded Undetected chrome') 75 | logging.info('Opening chatgpt') 76 | if cookie: 77 | self.browser.get('https://chat.openai.com/') 78 | self.browser.delete_all_cookies() 79 | for _cookie in cookie: 80 | if _cookie['name'] == '__Host-next-auth.csrf-token': 81 | continue 82 | self.browser.add_cookie(_cookie) 83 | self.browser.get(url) 84 | try: 85 | # Pass introduction 86 | next_button = WebDriverWait(self.browser, 5).until( 87 | EC.presence_of_element_located((By.XPATH, self.info_screen_xpath)) 88 | ) 89 | next_button.find_elements(By.TAG_NAME, self.button_tq)[0].click() 90 | 91 | next_button = WebDriverWait(self.browser, 5).until( 92 | EC.presence_of_element_located((By.XPATH, self.info_screen_xpath)) 93 | ) 94 | next_button.find_elements(By.TAG_NAME, self.button_tq)[1].click() 95 | 96 | next_button = WebDriverWait(self.browser, 5).until( 97 | EC.presence_of_element_located((By.XPATH, self.info_screen_xpath)) 98 | ) 99 | next_button.find_elements(By.TAG_NAME, self.button_tq)[1].click() 100 | logging.info('Info screen passed') 101 | except Exceptions.TimeoutException: 102 | logging.info('Info screen skipped') 103 | except Exception as exp: 104 | logging.error(f'Something unexpected happened: {exp}\n') 105 | 106 | else: 107 | self.browser.get('https://chat.openai.com/auth/login?next=/chat') 108 | if not cold_start: 109 | self.pass_verification() 110 | self.login(username, password) 111 | 112 | # turn to GPT4 by clicking the button 113 | if use_gpt4: 114 | gpt4button = self.sleepy_find_element(By.XPATH, '/html/body/div[1]/div[2]/div[2]/div/main/div[2]/div/div/div[1]/div/div/ul/li[2]/button') 115 | gpt4button.click() 116 | 117 | logging.info('ChatGPT is ready to interact') 118 | 119 | def pass_verification(self): 120 | ''' 121 | Performs the verification process on the page if challenge is present. 122 | 123 | This function checks if the login page is displayed in the browser. 124 | In that case, it looks for the verification button. 125 | This process is repeated until the login page is no longer displayed. 126 | 127 | Returns: 128 | None 129 | ''' 130 | while self.check_login_page(): 131 | verify_button = self.browser.find_elements(By.ID, 'challenge-stage') 132 | if len(verify_button): 133 | try: 134 | verify_button[0].click() 135 | logging.info('Clicked verification button') 136 | except Exceptions.ElementNotInteractableException: 137 | logging.info('Verification button is not present or clickable') 138 | time.sleep(1) 139 | return 140 | 141 | def check_login_page(self): 142 | ''' 143 | Checks if the login page is displayed in the browser. 144 | 145 | Returns: 146 | bool: True if the login page is not present, False otherwise. 147 | ''' 148 | login_button = self.browser.find_elements(By.XPATH, self.login_xq) 149 | return len(login_button) == 0 150 | 151 | def login(self, username :str, password :str): 152 | ''' 153 | Performs the login process with the provided username and password. 154 | 155 | This function operates on the login page. 156 | It finds and clicks the login button, 157 | fills in the email and password textboxes 158 | 159 | Args: 160 | username (str): The username to be entered. 161 | password (str): The password to be entered. 162 | 163 | Returns: 164 | None 165 | ''' 166 | 167 | # Find login button, click it 168 | login_button = self.sleepy_find_element(By.XPATH, self.login_xq) 169 | login_button.click() 170 | logging.info('Clicked login button') 171 | time.sleep(1) 172 | 173 | # Find email textbox, enter e-mail 174 | email_box = self.sleepy_find_element(By.ID, 'username') 175 | email_box.send_keys(username) 176 | logging.info('Filled email box') 177 | 178 | # Click continue 179 | continue_button = self.sleepy_find_element(By.XPATH, self.continue_xq) 180 | continue_button.click() 181 | time.sleep(1) 182 | logging.info('Clicked continue button') 183 | 184 | # Find password textbox, enter password 185 | pass_box = self.sleepy_find_element(By.ID, 'password') 186 | pass_box.send_keys(password) 187 | logging.info('Filled password box') 188 | # Click continue 189 | continue_button = self.sleepy_find_element(By.XPATH, self.continue_xq) 190 | continue_button.click() 191 | time.sleep(1) 192 | logging.info('Logged in') 193 | 194 | try: 195 | # Pass introduction 196 | next_button = WebDriverWait(self.browser, 5).until( 197 | EC.presence_of_element_located((By.CLASS_NAME, self.next_cq)) 198 | ) 199 | next_button.find_elements(By.TAG_NAME, self.button_tq)[0].click() 200 | 201 | next_button = WebDriverWait(self.browser, 5).until( 202 | EC.presence_of_element_located((By.CLASS_NAME, self.next_cq)) 203 | ) 204 | next_button.find_elements(By.TAG_NAME, self.button_tq)[1].click() 205 | 206 | next_button = WebDriverWait(self.browser, 5).until( 207 | EC.presence_of_element_located((By.CLASS_NAME, self.next_cq)) 208 | ) 209 | next_button.find_elements(By.TAG_NAME, self.button_tq)[1].click() 210 | logging.info('Info screen passed') 211 | except Exceptions.TimeoutException: 212 | logging.info('Info screen skipped') 213 | except Exception as exp: 214 | logging.error(f'Something unexpected happened: {exp}') 215 | 216 | def sleepy_find_element(self, by, query, attempt_count :int =20, sleep_duration :int =1): 217 | ''' 218 | Finds the web element using the locator and query. 219 | 220 | This function attempts to find the element multiple times with a specified 221 | sleep duration between attempts. If the element is found, the function returns the element. 222 | 223 | Args: 224 | by (selenium.webdriver.common.by.By): The method used to locate the element. 225 | query (str): The query string to locate the element. 226 | attempt_count (int, optional): The number of attempts to find the element. Default: 20. 227 | sleep_duration (int, optional): The duration to sleep between attempts. Default: 1. 228 | 229 | Returns: 230 | selenium.webdriver.remote.webelement.WebElement: Web element or None if not found. 231 | ''' 232 | for _count in range(attempt_count): 233 | item = self.browser.find_elements(by, query) 234 | if len(item) > 0: 235 | item = item[0] 236 | logging.info(f'Element {query} has found') 237 | break 238 | logging.info(f'Element {query} is not present, attempt: {_count+1}') 239 | time.sleep(sleep_duration) 240 | return item 241 | 242 | def wait_to_disappear(self, by, query, sleep_duration=1): 243 | ''' 244 | Waits until the specified web element disappears from the page. 245 | 246 | This function continuously checks for the presence of a web element. 247 | It waits until the element is no longer present on the page. 248 | Once the element has disappeared, the function returns. 249 | 250 | Args: 251 | by (selenium.webdriver.common.by.By): The method used to locate the element. 252 | query (str): The query string to locate the element. 253 | sleep_duration (int, optional): The duration to sleep between checks. Default: 1. 254 | 255 | Returns: 256 | None 257 | ''' 258 | 259 | while True: 260 | thinking = self.browser.find_elements(by, query) 261 | if len(thinking) == 0: 262 | logging.info(f'Element {query} is present, waiting') 263 | break 264 | time.sleep(sleep_duration) 265 | return 266 | 267 | def interact(self, question : str): 268 | ''' 269 | Sends a question and retrieves the answer from the ChatGPT system. 270 | 271 | This function interacts with the ChatGPT. 272 | It takes the question as input and sends it to the system. 273 | The question may contain multiple lines separated by '\n'. 274 | In this case, the function simulates pressing SHIFT+ENTER for each line. 275 | 276 | After sending the question, the function waits for the answer. 277 | Once the response is ready, the response is returned. 278 | 279 | Args: 280 | question (str): The interaction text. 281 | 282 | Returns: 283 | str: The generated answer. 284 | ''' 285 | 286 | # If the browser cannot find the text area, regenerate the response 287 | # 如果浏览器找不到输入框,说明上一次交互失败,重新生成 288 | try: 289 | text_area = self.browser.find_element(By.TAG_NAME, 'textarea') 290 | except Exception as exp: 291 | logging.error('Try regenerating') 292 | self.regenerate_response() 293 | return 'error' 294 | for each_line in question.split('\n'): 295 | text_area.send_keys(each_line) 296 | text_area.send_keys(Keys.SHIFT + Keys.ENTER) 297 | text_area.send_keys(Keys.RETURN) 298 | logging.info('Message sent, waiting for response') 299 | time.sleep(self.answer_wating_time) 300 | self.wait_to_disappear(By.CLASS_NAME, self.wait_cq) 301 | answer = self.browser.find_elements(By.CLASS_NAME, self.chatbox_cq)[-1] 302 | logging.info('Answer is ready') 303 | return answer.text 304 | 305 | def reset_thread(self): 306 | '''Function to close the current thread and start new one''' 307 | self.browser.find_element(By.XPATH, self.reset_xq).click() 308 | logging.info('New thread is ready') 309 | 310 | def regenerate_response(self): 311 | ''' 312 | Closes the current thread and starts a new one. 313 | 314 | Args: 315 | None 316 | 317 | Returns: 318 | None 319 | ''' 320 | try: 321 | regen_button = self.browser.find_element(By.XPATH, self.regen_xq) 322 | regen_button.click() 323 | logging.info('Clicked regenerate button') 324 | self.wait_to_disappear(By.CLASS_NAME, self.wait_cq) 325 | answer = self.browser.find_elements(By.CLASS_NAME, self.chatbox_cq)[-1] 326 | logging.info('New answer is ready') 327 | except Exceptions.NoSuchElementException: 328 | logging.error('Regenerate button is not present') 329 | return answer 330 | 331 | def close(self): 332 | '''Closes the browser and ends the session''' 333 | self.browser.close() 334 | self.browser.quit() 335 | logging.info('Browser is closed') 336 | return 337 | 338 | if __name__ == '__main__': 339 | import argparse 340 | parser = argparse.ArgumentParser() 341 | parser.add_argument('username') 342 | parser.add_argument('password') 343 | args = parser.parse_args() 344 | 345 | chatgpt = ChatGPT_Client(args.username, args.password) 346 | result = chatgpt.interact('Hello, how are you today') 347 | print(result) 348 | -------------------------------------------------------------------------------- /chatgpt_automation/helpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | import logging 4 | 5 | def detect_chrome_version(): 6 | ''' 7 | Detects chrome version, only supports linux and mac machines. 8 | If the command return something else than expected output, it uses the default version 112. 9 | ''' 10 | out = subprocess.check_output(['google-chrome', '--version']) 11 | out = re.search(r'Google\s+Chrome\s+(\d{3})', out.decode()) 12 | _v = 112 13 | if not out: 14 | logging.info('Could\'nt locate chrome version, using default value: 112') 15 | else: 16 | _v = int(out.group(1)) 17 | logging.info(f'The version is {_v}') 18 | 19 | return _v 20 | -------------------------------------------------------------------------------- /chatgpt_automation/talking_heads.py: -------------------------------------------------------------------------------- 1 | import time 2 | from .chatgpt_client import ChatGPT_Client 3 | 4 | class TalkingHeads: 5 | """An interface for talking heads""" 6 | def __init__(self, username: str, password: str, headless=False, head_count=2): 7 | self.head_count=head_count 8 | self.driver = ChatGPT_Client(username, password, headless) 9 | for _ in range(head_count-1): 10 | self.driver.browser.execute_script( 11 | '''window.open("https://chat.openai.com/chat","_blank");''') 12 | time.sleep(1) 13 | 14 | self.head_responses = [[] for _ in range(head_count)] 15 | 16 | def switch_to_tab(self, idx: int = 0): 17 | "Switch to tab" 18 | windows = self.driver.browser.window_handles 19 | if idx > len(windows): 20 | print(f"There is no tab with index {idx}") 21 | return 22 | self.driver.browser.switch_to.window(windows[idx]) 23 | 24 | def interact(self, head_number, question): 25 | """interact with the given head""" 26 | self.switch_to_tab(head_number) 27 | response = self.driver.interact(question) 28 | return response 29 | 30 | def reset_all_threads(self): 31 | """reset heads for the given number""" 32 | for head in range(self.head_count): 33 | self.switch_to_tab(head) 34 | self.driver.reset_thread() 35 | 36 | def start_conversation(self, text_1: str, text_2: str, use_response_1: bool= True): 37 | """Starts a conversation between two heads""" 38 | assert len(self.head_count) >= 2, "At least 2 heads is neccessary for a conversation" 39 | 40 | f_response = self.interact(0, text_1) 41 | text_2 = text_2 + f_response if use_response_1 else text_2 42 | s_response = self.interact(1, text_2) 43 | 44 | self.head_responses[0].append(f_response) 45 | self.head_responses[1].append(s_response) 46 | 47 | return f_response, s_response 48 | 49 | def continue_conversation(self, text_1: str= None, text_2: str= None): 50 | """Make another round of conversation. 51 | If text_1 or text_2 is given, the response is not used""" 52 | text_1 = text_1 or self.head_responses[1][-1] 53 | 54 | f_response = self.interact(0, text_1) 55 | text_2 = text_2 or f_response 56 | 57 | s_response = self.interact(1, text_2) 58 | 59 | self.head_responses[0].append(f_response) 60 | self.head_responses[1].append(s_response) 61 | return f_response, s_response 62 | -------------------------------------------------------------------------------- /configs.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | translated_file_dir = 'translated' 5 | text_dir = 'text' 6 | 7 | all_text = os.listdir(text_dir) 8 | 9 | total_accounts = 1 # 总共账号数量 10 | 11 | chrome_version = 113 # Chrome的大版本 12 | 13 | # 请在这里放置session的url。 14 | urls = { 15 | 1: 'https://chat.openai.com/c/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 16 | } 17 | 18 | # 请在这里放置cookies。 19 | cookies_mapping = {1: 'cookie_example.json'} 20 | 21 | prompt = 'please translate the following galgame conversations from Japanese to Chinese. ' + \ 22 | 'Please preserve escape characters like \\r\\n or \\" and strictly keep the JSON format of the provided content. ' + \ 23 | 'Please directly give the translated content only. ' +\ 24 | 'Here are the conversations:\n' 25 | 26 | # the time to wait for the answer from the chatbot. If the bot waits too short, error may occur. 27 | # 等待聊天机器人回答的时间。如果等待时间太短,可能会出现错误。 28 | answer_waiting_time = 10 29 | 30 | # the maximum number of lines to input to ChatGPT each time 31 | # 每次输入ChatGPT的最大行数 32 | batch_size = 15 -------------------------------------------------------------------------------- /cookie_example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "domain": ".openai.com", 4 | "expirationDate": 1686369036.078402, 5 | "hostOnly": false, 6 | "httpOnly": false, 7 | "name": "_puid", 8 | "path": "/", 9 | "sameSite": "lax", 10 | "secure": true, 11 | "session": false, 12 | "storeId": null, 13 | "value": "user-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 14 | }, 15 | { 16 | "domain": ".openai.com", 17 | "expirationDate": 1686369038, 18 | "hostOnly": false, 19 | "httpOnly": false, 20 | "name": "intercom-session-dgkjq2bp", 21 | "path": "/", 22 | "sameSite": "lax", 23 | "secure": false, 24 | "session": false, 25 | "storeId": null, 26 | "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 27 | }, 28 | { 29 | "domain": ".chat.openai.com", 30 | "hostOnly": false, 31 | "httpOnly": true, 32 | "name": "_cfuvid", 33 | "path": "/", 34 | "sameSite": "no_restriction", 35 | "secure": true, 36 | "session": true, 37 | "storeId": null, 38 | "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 39 | }, 40 | { 41 | "domain": ".openai.com", 42 | "expirationDate": 1709094238, 43 | "hostOnly": false, 44 | "httpOnly": false, 45 | "name": "intercom-device-id-dgkjq2bp", 46 | "path": "/", 47 | "sameSite": "lax", 48 | "secure": false, 49 | "session": false, 50 | "storeId": null, 51 | "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 52 | }, 53 | { 54 | "domain": "chat.openai.com", 55 | "expirationDate": 1688356250.707136, 56 | "hostOnly": true, 57 | "httpOnly": true, 58 | "name": "__Secure-next-auth.session-token", 59 | "path": "/", 60 | "sameSite": "lax", 61 | "secure": true, 62 | "session": false, 63 | "storeId": null, 64 | "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 65 | }, 66 | { 67 | "domain": ".chat.openai.com", 68 | "expirationDate": 1685766036.854741, 69 | "hostOnly": false, 70 | "httpOnly": true, 71 | "name": "__cf_bm", 72 | "path": "/", 73 | "sameSite": "no_restriction", 74 | "secure": true, 75 | "session": false, 76 | "storeId": null, 77 | "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 78 | }, 79 | { 80 | "domain": "chat.openai.com", 81 | "hostOnly": true, 82 | "httpOnly": true, 83 | "name": "__Secure-next-auth.callback-url", 84 | "path": "/", 85 | "sameSite": "lax", 86 | "secure": true, 87 | "session": true, 88 | "storeId": null, 89 | "value": "https%3A%2F%2Fchat.openai.com" 90 | }, 91 | { 92 | "domain": "chat.openai.com", 93 | "hostOnly": true, 94 | "httpOnly": true, 95 | "name": "__Host-next-auth.csrf-token", 96 | "path": "/", 97 | "sameSite": "lax", 98 | "secure": true, 99 | "session": true, 100 | "storeId": null, 101 | "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 102 | }, 103 | { 104 | "domain": "chat.openai.com", 105 | "expirationDate": 1685765150, 106 | "hostOnly": true, 107 | "httpOnly": false, 108 | "name": "_dd_s", 109 | "path": "/", 110 | "sameSite": "strict", 111 | "secure": false, 112 | "session": false, 113 | "storeId": null, 114 | "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 115 | }, 116 | { 117 | "domain": ".openai.com", 118 | "expirationDate": 1719345077.73398, 119 | "hostOnly": false, 120 | "httpOnly": false, 121 | "name": "_ga", 122 | "path": "/", 123 | "sameSite": null, 124 | "secure": false, 125 | "session": false, 126 | "storeId": null, 127 | "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 128 | }, 129 | { 130 | "domain": ".openai.com", 131 | "expirationDate": 1719345077.733494, 132 | "hostOnly": false, 133 | "httpOnly": false, 134 | "name": "_ga_9YTZJE58M9", 135 | "path": "/", 136 | "sameSite": null, 137 | "secure": false, 138 | "session": false, 139 | "storeId": null, 140 | "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 141 | } 142 | ] -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from translator import Translator 2 | import sys 3 | import os 4 | 5 | 6 | # get an argument from the console 7 | # the argument is the account number 8 | 9 | if __name__ == '__main__': 10 | if not os.path.exists('translated'): 11 | os.mkdir('translated') 12 | account = int(sys.argv[1]) 13 | start_time = sys.argv[2] 14 | translator = Translator(account, start_time) # 可以添加headless=False来显示浏览器,方便调试 15 | translator.start_translate() 16 | translator.chatgpt.close() -------------------------------------------------------------------------------- /proofread.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from translator import is_finished 4 | from translator import load_cookies 5 | from chatgpt_automation import ChatGPT_Client 6 | 7 | translated_file_dir = 'translated' 8 | text_file_dir = 'text' 9 | 10 | # check if the translated files are duplicated 11 | # 检查翻译后的文件是否有重复的内容 12 | def check_duplication(): 13 | for file in os.listdir(translated_file_dir): 14 | file_path = translated_file_dir + '\\' + file 15 | with open(file_path, 'r', encoding='utf-8') as f: 16 | try: 17 | data = json.load(f) 18 | text = [list(i.values())[0] for i in data] 19 | if len(text) - len(set(text)) > 5: 20 | # print the duplicated elements 21 | print(set([x for x in text if text.count(x) > 1])) 22 | print(file) 23 | except: 24 | pass 25 | 26 | 27 | def proofread_with_gpt3(stepsize=50): 28 | proofread_session_url = 'https://chat.openai.com/c/38149660-e541-4d00-8190-82024db7599e' 29 | proofread_cookies = load_cookies(2) 30 | 31 | chatgpt = ChatGPT_Client(proofread_cookies, url=proofread_session_url, answer_waiting_time=1) 32 | 33 | prompt = 'Please compare the following two texts which are in Chinese and Japanese. If they have the same meaning, please answer "1", otherwise answer "0".\n\nChinese:\n' 34 | 35 | for file in os.listdir(translated_file_dir): 36 | wrong_translation = [] 37 | if is_finished(file): 38 | with open(f'{translated_file_dir}/{file}', 'r', encoding='utf-8') as f: 39 | data = json.load(f) 40 | text = [list(i.values())[0] for i in data] 41 | with open(f'{text_file_dir}/{file}', 'r', encoding='utf-8') as f: 42 | raw_text = json.load(f) 43 | raw_text = [list(i.values())[0] for i in raw_text] 44 | for i in range(0, len(text), stepsize): 45 | question = prompt + text[i] + '\n\nJapanese:\n' + raw_text[i] 46 | answer = chatgpt.interact(question) 47 | if answer == '0': 48 | wrong_translation.append((file, i)) 49 | print(f'file: {file}, index: {i}') 50 | print(question) 51 | print(answer) 52 | print('-------------------------') 53 | print() 54 | else: 55 | print(f'file: {file}, index: {i}') 56 | print(question) 57 | print(answer) 58 | print('-------------------------') 59 | print() 60 | wrongs = "\n".join([str(i) for i in wrong_translation]) 61 | print(f'results:\n{wrongs}') 62 | 63 | 64 | if __name__ == '__main__': 65 | proofread_with_gpt3() 66 | # check_duplication() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium==4.9.1 2 | undetected_chromedriver==3.4.7 3 | -------------------------------------------------------------------------------- /text/01_01.json: -------------------------------------------------------------------------------- 1 | [{"静流":"「いらっしゃい」"}, 2 | {"???":"「……」"}, 3 | {"旁白":"一人の男性客が店に入るなり黙ってその場で立ち止まる。"}, 4 | {"静流":"「ん? どうしました?」"}, 5 | {"???":"「あ、いや、キマイラの店主さんがおかえりだって聞いていたので……」"}, 6 | {"静流":"「あれ? 君は、あれかな? 私がいない間のキマイラの常連さん?」"}, 7 | {"???":"「あ、まぁ、そんな感じですかね」"}, 8 | {"旁白":"ずいぶんとすらりとしたイケメンだ。"}, 9 | {"旁白":"初対面なのだが、どこかで会った事がある様な。"}, 10 | {"静流":"「そうですか。円木ちゃんが店長してた時からの常連さんですか?」"}, 11 | {"???":"「あ、いや、その前、鳥谷真琴って娘が切り盛りしていた時代からです」"}, 12 | {"旁白":"鳥谷真琴? そりゃ、ずいぶん昔の話だ。\r\nかれこれ10年ぐらい前じゃないだろうか?"}, 13 | {"静流":"「そりゃまた古い常連さんですね。\r\nまぁ、座ってくださいよ」"}, 14 | {"???":"「そうですね」"}, 15 | {"旁白":"男性客はカウンターに座る。"}, 16 | {"旁白":"誰かに似ているな?"}, 17 | {"旁白":"私はそんな事を思った。"}, 18 | {"静流":"「それで常連さん、お名前聞いていいかな?」"}, 19 | {"直哉":"「ああ、草薙直哉といいます」"}, 20 | {"旁白":"私はその場で息をのんだ。"}, 21 | {"静流":"「草薙!? 直哉ぁ!?」"}, 22 | {"直哉":"「え?」"}, 23 | {"静流":"「え?」"}, 24 | {"直哉":"「も、もしかして、なんか悪い噂とか聞いてたりします?」"}, 25 | {"静流":"「え? なんでそうなる?」"}, 26 | {"直哉":"「あ、いや、驚き方が尋常じゃ無かったんで……」"}, 27 | {"旁白":"たしかに、初対面の人間に対してだったら、今の驚き方は尋常じゃなかったかも\r\nしれない。ただ、私にとって、その名はそれほどの反応があって然りのものであったのだ。"}, 28 | {"静流":"「いやね。真琴って私のいとこにあたるのよ。\r\nまぁ、君の話も聞いた事があるからさ……」"}, 29 | {"直哉":"「そうですか」"}, 30 | {"旁白":"彼はやや納得しがたいという感じではあったが、\r\nそれでもそれ以上追求してこなかった。"}, 31 | {"旁白":"とは言っても、隠す事でもないのだが、私が驚いた理由を話すには、長いというか、いろいろあるというか。"}, 32 | {"旁白":"まぁ、人生いろいろあるからね!"}, 33 | {"静流":"「それで、ご注文は?」"}, 34 | {"直哉":"「牛丼をお願いします」"}, 35 | {"静流":"「牛丼ね。ちょっと待ってて」"}, 36 | {"旁白":"私は厨房に戻る。"}, 37 | {"旁白":"冷蔵庫に入れられた鍋の一つを取り出して火にかけ、少しの間、私は考えた。"}, 38 | {"静流":"「草薙直哉か……」"}, 39 | {"静流":"「なるほどね。なるほど……」"}, 40 | {"旁白":"草薙健一郎の息子。"}, 41 | {"旁白":"面影はある。"}, 42 | {"旁白":"むしろ、彼に子供がいたら、あんな感じなんだろうなぁ。と思わせるぐらいだ。"}, 43 | {"旁白":"などと考えていると――"}, 44 | {"静流":"「っと、煮立ってるっっ」"}, 45 | {"旁白":"急いで火を弱めて、ザルで肉をすくう。\r\n少し、多め。"}, 46 | {"旁白":"さっとどんぶりを拭き、カウンターに持っていく。"}, 47 | {"静流":"「はい、おまちどう」"}, 48 | {"直哉":"「って! デカっ」"}, 49 | {"静流":"「ちょっとサービスしてあげた」"}, 50 | {"直哉":"「少しって、肉山盛りじゃないですか……」"}, 51 | {"静流":"「まぁね。そんだけ量が多いと食べるのに時間かかるでしょ?」"}, 52 | {"直哉":"「そんな問題!?」"}, 53 | {"静流":"「うん、そうしたらお話できるじゃない」"}, 54 | {"直哉":"「せっかくの料理が冷めてしまいますよ」"}, 55 | {"静流":"「君は私の質問に頷いてればいいよ」"}, 56 | {"直哉":"「質問って……、なんかやっぱり俺に悪い印象とかあります?」"}, 57 | {"旁白":"草薙直哉は上目遣いで私を見つめる。"}, 58 | {"静流":"「違う、違う、そういう尋問みたいなもんじゃないからさ、とりあえず食べてよ」"}, 59 | {"直哉":"「は、はぁ……」"}, 60 | {"旁白":"あまり納得した風ではなかったが、彼は牛丼を食べ始めた。"}, 61 | {"旁白":"でかい手だな。と私は思った。\r\nあの時も同じ事を思ったっけ……。"}, 62 | {"旁白":"あの手の感触を私は忘れない。"}, 63 | {"静流":"「不思議なもんだね。そんな大きな手であんな繊細な絵を描くんだからさ」"}, 64 | {"直哉":"「え? 俺の絵とか知ってるんですか?」"}, 65 | {"静流":"「そりゃ、この弓張が誇る有名人の一人じゃない」"}, 66 | {"直哉":"「父親が有名なだけですよ」"}, 67 | {"静流":"「そうでもないよ。君だって大したもんじゃない。『蝶を夢む』なんか相当な名作よ」"}, 68 | {"直哉":"「良くそんな作品知ってますね。十年ぐらい前のもんですよ」"}, 69 | {"静流":"「あの年のムーア展は熱かったねぇ。当時、私はコスタリカのサンホセにいたんだけどさ。それでもニュースでやってたよ。日本から受賞者が三人も出たって」"}, 70 | {"静流":"「しかも、そのうちの二人が弓張学園の学生だっていうんだからさ」"}, 71 | {"直哉":"「そうですね。後にも先にも国内であれだけ騒がれた芸術イベントも無かった\r\nですね。でも静流さんって国外にいたのでしょ? \r\nノミネートでしかない俺の作品なんて良く知ってましたね」"}, 72 | {"静流":"「だってムーア展って、毎年ニューヨークでノミネート作品も含めた展覧会やって\r\nいるじゃない。近くだったから見に行ったんだよ」"}, 73 | {"直哉":"「近く? コスタリカってアフリカじゃありませんでしたっけ?」"}, 74 | {"静流":"「わー、私も方向音痴で大概だけど、あんたもひどいもんだねぇ。\r\nコスタリカは南米だよ」"}, 75 | {"静流":"「ちなみに私が期待したのは\r\n『アメリカ大陸ってだけで全然近くないじゃないですか』\r\nってツッコミだったんだけどな」"}, 76 | {"直哉":"「まぁ、静流さんが世界中を旅しているとは聞いてますから、\r\nそんな人にとってはアフリカからニューヨークも近いのかなぁ……と」"}, 77 | {"静流":"「そんなわけあるかっ。\r\nって、私がツッコミ役か!」"}, 78 | {"直哉":"「すみません。地理関係は本当に疎くて、マジボケです」"}, 79 | {"旁白":"地理に疎い。"}, 80 | {"旁白":"そういえば、そんな話を聞いている。"}, 81 | {"旁白":"というか、この子に(無自覚に)好意を抱いてるヤツは多いからな。\r\n細かい話まで知っている。"}, 82 | {"静流":"「今日は再開した初日だけど、良く来店してくれたね」"}, 83 | {"直哉":"「張り紙に再開日が書いてありましたし、それに円木さんからオーナーの静流さんが帰ってくると聞いていたので、一度挨拶しておきたいなぁと」"}, 84 | {"旁白":"円木つぐみ。\r\nずっとこのキマイラの雇われ店長をしていた娘だ。"}, 85 | {"旁白":"真琴と同時期にバイトに入ってもらっていた娘だが、\r\n真琴が弓張を離れた後も、この店をずっと切り盛りしてくれていた。"}, 86 | {"直哉":"「円木さんもご結婚されてお子さんも出来たという事で」"}, 87 | {"静流":"「結婚してもこの店やっててくれたんだけどねぇ。\r\nさすがに妊婦に仕事させるわけにはいかないからねぇ」"}, 88 | {"直哉":"「そんなこんなで、とうとう観念して帰国したわけですか?」"}, 89 | {"静流":"「良く分かったね。まだまだ海外でやりたい事多かったんだけどさぁ」"}, 90 | {"直哉":"「静流さんがこの店のオーナーなのは知ってますけど、生計をそれだけで立ててたのですか?」"}, 91 | {"静流":"「まさか。ちゃんと私は私で仕事をしていたよ」"}, 92 | {"直哉":"「コーヒー以外?」"}, 93 | {"静流":"「いいや、コーシーだよ」"}, 94 | {"直哉":"「コーシー? また古い言い方ですね」"}, 95 | {"静流":"「キマイラのもともとの店長が戦前の東京生まれでね。彼がいつもコーヒーの事を\r\nコーシーと言っていた」"}, 96 | {"静流":"「まぁ、そんなこんなで私にとってキマイラで出るコーヒーはコーシーなんだよ。\r\nとは言っても、メニューにはコーヒーと書いてあるけどね」"}, 97 | {"直哉":"「コーヒーの仕事って主に何を?」"}, 98 | {"静流":"「あまり知られてないのだけどね。コーシーの価格はほとんどが輸送費なんだよ。\r\nだから現地で言う原価とこっちで言う原価は全然違う。\r\n流通経路を押さえて輸送コストを削ると、コーシーというのは格段に安くなる」"}, 99 | {"直哉":"「へぇ、そうなんですか」"}, 100 | {"静流":"「そういうコンサル的な仕事をしていた。\r\nこのキマイラではいろいろな豆を送って使ってもらっていたが、まぁ、市場調査みたいなもんだね。だから通常よりも格安でめずらしいコーシーが飲める」"}, 101 | {"直哉":"「この店のオーナーってだけじゃなかったんですか」"}, 102 | {"静流":"「そうだよ。このお店の売り上げだけで、世界中放浪できるほど世の中甘くないよ。\r\nというか、収入のほとんどはコンサルだった」"}, 103 | {"直哉":"「海外でコンサルとか聞くと、なんかやり手のエリートっぽいですね」"}, 104 | {"静流":"「全然、最終学歴は二年留年したあげくのやっとの弓張卒だし」"}, 105 | {"直哉":"「そうなんですか?」"}, 106 | {"静流":"「うん、海外旅行にうつつを抜かしていたら出席日数が足りなくなってね。\r\nなんか知らんが二回も留年してしまった」"}, 107 | {"直哉":"「その頃から海外だったんですね」"}, 108 | {"静流":"「そうだね。まぁ、あの頃はコーシーが目的では無かったのだけど――」"}, 109 | {"直哉":"「……」"}, 110 | {"旁白":"では何が? と聞かれそうだった。"}, 111 | {"静流":"「それより、君は今何をやってるのかね?」"}, 112 | {"旁白":"ので、すぐに切り替えた。"}, 113 | {"直哉":"「その弓張学園で教師をやってます。とは言っても正規雇用じゃないのですけどね」"}, 114 | {"静流":"「ああ、そういえば、そんな話も聞いていたかも。\r\n世知辛いもんだね。紗希おばさんももう少し優しくしてあげても良さそうなものだけど」"}, 115 | {"直哉":"「紗希おばさん? そういえば静流さんって校長の姪にあたりましたっけ?」"}, 116 | {"静流":"「うん、そうだよ。真琴から聞いてるでしょ?」"}, 117 | {"直哉":"「いや、鳥谷からはそこまでは、校長から一度そんな話を聞いた事があって」"}, 118 | {"静流":"「ふーん。紗希おばさんとは仲は良いのかい?」"}, 119 | {"直哉":"「仲が良いというか、雇用主と被雇用者ですからねぇ」"}, 120 | {"静流":"「それもそうか……」"}, 121 | {"旁白":"窓の外から弓張の鐘が聞こえる。"}, 122 | {"静流":"「昼休みが終わる鐘が鳴っているが、君は大丈夫なのか?」"}, 123 | {"直哉":"「話を長引かせようと牛丼を大盛りにしてくれたのは静流さんじゃないですか」"}, 124 | {"静流":"「あはは、そうだったね」"}, 125 | {"直哉":"「授業なら問題ありませんよ。午後から美術の授業なんてありませんから」"}, 126 | {"静流":"「なるほどね」"}, 127 | {"旁白":"草薙直哉はゆっくりと牛丼を食す。"}, 128 | {"旁白":"弓張の教師か。\r\nだったらこれからもこの店の常連になるな。"}, 129 | {"直哉":"「海外生活はどうでしたか?」"}, 130 | {"静流":"「放浪生活が板についてしまっていてね。\r\n今の生活に慣れるのが大変なぐらいだよ」"}, 131 | {"直哉":"「俺の父親もずっと海外でしたよ」"}, 132 | {"静流":"「そうだったね。草薙健一郎さんはほとんど日本にいなかったらしいね」"}, 133 | {"直哉":"「はい、みんなこの土地を離れていきますよ」"}, 134 | {"旁白":"草薙直哉はそんな事を言いながら、弓張の鐘のした方向を見つめていた。\r\nもう、チャイムの音はそこにはない。"}, 135 | {"静流":"「だけど、私みたいに帰ってくる人間もいるじゃない」"}, 136 | {"直哉":"「俺みたいに、弓張から全然外に出ない人間もいますけどね」"}, 137 | {"静流":"「それって、郷土愛的な感じ?」"}, 138 | {"直哉":"「どうでしょうかね。案外恐がりなだけかもしれませんよ」"}, 139 | {"静流":"「恐がり?」"}, 140 | {"直哉":"「いろいろな人がここを去っていきましたからね。\r\n恐がりというか寂しがり屋なのかもしれないですね」"}, 141 | {"静流":"「なるほど、君は面白い言い回しをするんだね」"}, 142 | {"旁白":"草薙直哉とはその後、三十分ほど話した。"}, 143 | {"旁白":"探りを入れた。\r\nというわけではないが、なんでもかんでも彼に聞くのは無粋とも思えた。\r\n彼の心がざわつかない程度の質問だけをした。"}, 144 | {"旁白":"だって、彼とははじめて会って、\r\nにもかかわらず、私は彼の事をいろいろと聞かされている。"}, 145 | {"旁白":"たぶん、彼が知らない、彼に関わる事だって知っているのだろう。"}, 146 | {"旁白":"彼が思う以上に私と夏目の人間は関係が深い。"}, 147 | {"直哉":"「それじゃ、そろそろ帰ります」"}, 148 | {"旁白":"そう言って草薙直哉は立ち上がり、財布に手をかける。"}, 149 | {"旁白":"会計にたった時、\r\nレジの脇に置かれていたものに、彼は目を奪われていた。"}, 150 | {"直哉":"「そういえば……これ」"}, 151 | {"旁白":"そこには一つの花瓶があった。"}, 152 | {"直哉":"「今まで無かったですね」"}, 153 | {"静流":"「ああ、そうだね」"}, 154 | {"直哉":"「素晴らしい彩釉です。\r\nかなりめずらしいものですね。\r\nもしかして、海外のものなのですか?」"}, 155 | {"静流":"「そう見えるか?」"}, 156 | {"旁白":"彼は少しだけ考えた後に、首を横に振った。"}, 157 | {"直哉":"「かなり長い時間この店に通っていますが、見た事もない花瓶だったので、静流さんの海外のお土産かと」"}, 158 | {"静流":"「いや、日本のものだよ。\r\nこれはねぇ。今まで隠してきたのさ」"}, 159 | {"直哉":"「何故ですか?」"}, 160 | {"静流":"「知られるとねぇ。相続税が大変な事になるかもしれないからだよ」"}, 161 | {"直哉":"「なんですか? それ?\r\nまるで文化財になるほどの逸品みたいな言い方じゃないですか」"}, 162 | {"静流":"「ああ、そうだね。文化財になるかもしれない」"}, 163 | {"旁白":"草薙直哉はその花瓶を良く見る。"}, 164 | {"旁白":"そして少しだけ苦笑いした後に、"}, 165 | {"直哉":"「素晴らしい出来ですが、現代作家のものです。\r\nたぶん作者は陶芸家、吉沢緑寺でしょうね」"}, 166 | {"静流":"「なぜそう思う?」"}, 167 | {"直哉":"「海を表現した独特な手法はバルボティーヌ。\r\nうねりや波の表現なども表面だけじゃなくて全体で表現している」"}, 168 | {"直哉":"「だけど、一番重要なのはこの彩釉ですね。これは碧緋と言われる独特なものです」"}, 169 | {"直哉":"「世界で彼だけがこの独特な彩釉、碧緋を出すことができる。\r\n逆に彼以外でこれを出すことに成功した人物はいない」"}, 170 | {"旁白":"私はその指摘に思わず笑ってしまう。"}, 171 | {"旁白":"すると、草薙直哉はやれやれといった感じで、"}, 172 | {"直哉":"「まぁ、静流さんが吉沢緑寺を信じているのなら否定はしません。\r\n素晴らしい陶芸家ではありますし、それこそいつか人間国宝になって世界的な\r\n陶芸家とよばれ、この花瓶も文化財指定される日も来るかもしれませんからね」"}, 173 | {"静流":"「そうだね、たしかに吉沢緑寺先生はすごい人ではあるよ」"}, 174 | {"旁白":"私の言い回しに、彼は少しだけ不思議そうな顔をした。"}, 175 | {"旁白":"彼はこの様にしてキマイラを後にした。"}, 176 | {"旁白":"私はその夜に電話した。"}, 177 | {"静流":"「あ、もしもし、ひさしぶり、鳥谷静流だ。\r\nそっちはどうよ? 藍」"}, 178 | {"旁白":"私の親友に――"}] -------------------------------------------------------------------------------- /translator.py: -------------------------------------------------------------------------------- 1 | from chatgpt_automation import ChatGPT_Client 2 | import json 3 | import logging 4 | import time 5 | import os 6 | from configs import * 7 | 8 | 9 | def list_untranslated_files(raw_files:list[str]): 10 | untranslated_files = [i for i in raw_files if not is_finished(i)] 11 | return untranslated_files 12 | 13 | 14 | def is_finished(filename): 15 | translated_files = os.listdir(translated_file_dir) 16 | if filename not in translated_files: 17 | return False 18 | 19 | with open(f'{translated_file_dir}/{filename}', 'r', encoding='utf-8') as f: 20 | try: 21 | translated_text = json.load(f) 22 | except: 23 | return False 24 | 25 | with open(f'{text_dir}/{filename}', 'r', encoding='utf-8') as f: 26 | raw_text = json.load(f) 27 | return len(translated_text) == len(raw_text) 28 | 29 | 30 | def load_cookies(account: int): 31 | cookies_file = cookies_mapping[account] 32 | mapp = {'lax': 'Lax', 'strict': 'Strict', 'no_restriction': 'None', None: 'None'} 33 | with open(cookies_file, 'r', encoding='utf-8') as f: 34 | cookies = json.load(f) 35 | for i in range(len(cookies)): 36 | cookies[i]['sameSite'] = mapp[cookies[i]['sameSite']] 37 | return cookies 38 | 39 | 40 | class Translator: 41 | def __init__(self, account: int, start_time:str, headless=True): 42 | self.account = account 43 | self.headless = headless 44 | 45 | print(f'account {account} will start at {start_time}...') 46 | # wait until start_time. The start_time is in the format of 'hh:mm:ss' 47 | while time.strftime('%H:%M:%S', time.localtime()) < start_time: 48 | time.sleep(60) 49 | 50 | self.chatgpt: ChatGPT_Client = None 51 | 52 | self.time_list = [] 53 | self.start = 0 54 | 55 | 56 | def create_chatgpt(self): 57 | if self.chatgpt: 58 | self.chatgpt.close() 59 | 60 | # load cookies 61 | cookies = load_cookies(self.account) 62 | 63 | retry_times = 0 64 | while True: 65 | try: 66 | self.chatgpt = ChatGPT_Client( 67 | cookies, 68 | url=urls[self.account], 69 | use_gpt4=False, 70 | verbose=True, 71 | headless=self.headless, 72 | answer_waiting_time=answer_waiting_time, 73 | chrome_version=chrome_version 74 | ) 75 | except Exception as e: 76 | logging.info(e) 77 | if retry_times < 20: 78 | retry_times += 1 79 | try: 80 | self.chatgpt.close() 81 | except: 82 | pass 83 | logging.info(f'failed to connect to chatgpt, retrying {retry_times} times') 84 | time.sleep(300) 85 | else: 86 | logging.info('failed to connect to chatgpt, exiting...') 87 | exit() 88 | else: 89 | break 90 | 91 | 92 | def start_translate(self, _raw_text=None): 93 | while True: 94 | # Any error results in restarting the chatgpt and the translation process 95 | # 发生的任何错误都会导致重启chatgpt和翻译进程 96 | try: 97 | self.create_chatgpt() 98 | if not _raw_text: 99 | raw_text = list_untranslated_files([all_text[i] for i in range(len(all_text)) if i % total_accounts == self.account - 1]) 100 | else: 101 | raw_text = _raw_text 102 | if not raw_text: 103 | logging.info('all files have been translated, exiting...') 104 | exit() 105 | 106 | for filename in raw_text: 107 | if os.path.exists(f'{translated_file_dir}/{filename}'): 108 | if is_finished(filename): 109 | logging.info(f'{filename} has been translated, skipping...') 110 | continue 111 | with open(f'{translated_file_dir}/{filename}', 'r', encoding='utf-8') as f: 112 | translated_text = f.read()[1:-1] 113 | # the number of lines 114 | translated_lines = len(translated_text.split('\n')) - 1 115 | logging.info(f'continue {filename} from line {translated_lines}') 116 | else: 117 | translated_lines = 0 118 | translated_text = '' 119 | logging.info(f'start to translate {filename}') 120 | # divide text into batches 121 | with open(f'{text_dir}/{filename}', 'r', encoding='utf-8') as f: 122 | text = f.read()[1:-1] 123 | text = text.split('\n')[translated_lines:] 124 | total_lines = len(text) 125 | text = ['\n'.join(text[i:i + batch_size]) for i in range(0, len(text), batch_size)] 126 | 127 | 128 | for (i, batch) in enumerate(text): 129 | #============================================== 130 | if len(self.time_list) >= 8: 131 | self.start = self.time_list[-8] 132 | self.time_list.append(time.time()) 133 | # wait until start + 3 hour 134 | if time.time() < self.start + 3600: 135 | logging.info(f'limit reached, wait until {time.strftime("%H:%M:%S", time.localtime(self.start + 3600))}') 136 | while time.time() < self.start + 3600: 137 | time.sleep(60) 138 | self.create_chatgpt() 139 | 140 | answer = self.translate_once(batch) 141 | if 'error' in answer: 142 | logging.info('Error in response') 143 | raise Exception('Error in response') 144 | if 'cap' in answer: # reach the capacity. 达到了ChatGPT4上限 145 | self.start = time.time() 146 | logging.info('cap detected, wait for 1 hours') 147 | while time.time() < self.start + 3600: 148 | time.sleep(60) 149 | raise Exception('cap detected') 150 | if 'complete' in answer: # if the account is a shared account, this might happen. 如果是共享账号,这种情况可能发生 151 | logging.info('others are using the account, wait for 5 minutes') 152 | time.sleep(60) 153 | raise Exception('others are using the account') 154 | #============================================== 155 | if answer[-1] != ',' and batch != text[-1]: 156 | answer += ',\n' 157 | else: 158 | answer += '\n' 159 | translated_text += answer 160 | with open(f'{translated_file_dir}/{filename}', 'w', encoding='utf-8') as f: 161 | f.write(f'[{translated_text}]') 162 | logging.info(f'{(i+1)*batch_size} / {total_lines} finished in {filename}') 163 | except Exception as e: 164 | # if keyboard interrupt, exit 165 | if e == KeyboardInterrupt: 166 | break 167 | logging.info(e) 168 | 169 | def translate_once(self, batch:str): 170 | message = prompt + batch 171 | answer = self.chatgpt.interact(message) 172 | 173 | return answer 174 | 175 | 176 | if __name__ == '__main__': 177 | print('\n'.join(list_untranslated_files(os.listdir(text_dir)))) --------------------------------------------------------------------------------