diff --git a/modules/alice.py b/modules/alice.py index cf2c4ef..22da635 100644 --- a/modules/alice.py +++ b/modules/alice.py @@ -1,599 +1,600 @@ -# Libraries -import os -import re -import textwrap -import threading -import time -import traceback -import tiktoken -import openai -from pathlib import Path -from colorama import Fore -from dotenv import load_dotenv -from retry import retry -from tqdm import tqdm - -# Open AI -load_dotenv() -if os.getenv("api").replace(" ", "") != "": - openai.base_url = os.getenv("api") -openai.organization = os.getenv("org") -openai.api_key = os.getenv("key") - -# Globals -MODEL = os.getenv("model") -TIMEOUT = int(os.getenv("timeout")) -LANGUAGE = os.getenv("language").capitalize() -PROMPT = Path("prompt.txt").read_text(encoding="utf-8") -VOCAB = Path("vocab.txt").read_text(encoding="utf-8") -THREADS = int(os.getenv("threads")) -LOCK = threading.Lock() -WIDTH = int(os.getenv("width")) -LISTWIDTH = int(os.getenv("listWidth")) -NOTEWIDTH = 70 -MAXHISTORY = 10 -ESTIMATE = "" -TOKENS = [0, 0] -NAMESLIST = [] -NAMES = False # Output a list of all the character names found -BRFLAG = False # If the game uses
instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.01 - OUTPUTAPICOST = 0.03 - BATCHSIZE = 1 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleAlice(filename, estimate): - global ESTIMATE - totalTokens = [0, 0] - ESTIMATE = estimate - - if estimate: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - totalTokens[0] += translatedData[1][0] - totalTokens[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", totalTokens, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - else: - try: - with open("translated/" + filename, "w", encoding="UTF-8") as outFile: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - outFile.writelines(translatedData[0]) - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - totalTokens[0] += translatedData[1][0] - totalTokens[1] += translatedData[1][1] - except Exception: - traceback.print_exc() - return "Fail" - - return getResultString(["", totalTokens, None], end - start, "TOTAL") - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="UTF-8") as f: - translatedData = parseText(f, filename) - - return translatedData - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def parseText(data, filename): - # Get total for progress bar - linesList = data.readlines() - totalTokens = [0, 0] - totalLines = len(linesList) - global LOCK - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar: - pbar.desc = filename - pbar.total = totalLines - try: - result = translateLines(linesList, pbar) - totalTokens[0] += result[1][0] - totalTokens[1] += result[1][1] - except Exception as e: - traceback.print_exc() - return [linesList, totalTokens, e] - return [linesList, totalTokens, None] - - -# Grab scenario data from text file -def translateLines(linesList, pbar): - currentGroup = [] - batch = [] - textHistory = [] - tokens = [0, 0] - batchStartIndex = 0 - insertBool = False - multiLine = False - i = 0 - - try: - while i < len(linesList): - # Check if Proper Message - match = re.findall(r"s\[[0-9]+\] = \"(.*)\"", linesList[i]) - if len(match) > 0: - jaString = match[0] - - # Skip Files - if "/" in jaString: - i += 1 - continue - - ### Translate - # Remove any textwrap - jaString = re.sub(r"\\n", " ", jaString) - - # Grab Speaker - speakerMatch = re.findall(r"s\[[0-9]+\] = \"([^/]+)\"", linesList[i - 1]) - if len(speakerMatch) > 0: - # If there isn't any Japanese in the text just skip - if re.search(r"[一-龠]+|[ぁ-ゔ]+|[ァ-ヴー]+", jaString) and "_" not in speakerMatch[0]: - speaker = speakerMatch[0] - else: - speaker = "" - else: - speaker = "" - - # Grab rest of the messages - currentGroup.append(jaString) - - # Check if next line should be merged - if insertBool is True: - linesList[i] = re.sub(r"(s\[[0-9]+\]) = \"(.+)\"", r'\1 = ""', linesList[i]) - linesList[i] = linesList[i].replace(";", "") - start = i - while len(linesList) > i + 1 and re.search(r"s\[[0-9]+\] = \"\s+(.*)\"", linesList[i + 1]) != None: - multiLine = True - i += 1 - match = re.findall(r"s\[[0-9]+\] = \"\s+(.*)\"", linesList[i]) - currentGroup.append(match[0]) - if insertBool is True: - linesList[i] = re.sub(r"(s\[[0-9]+\]) = \"\s+(.+)\"", r'\1 = ""', linesList[i]) - linesList[i] = linesList[i].replace(";", "") - i += 1 - - # Combine Groups and Add Speaker - finalJAString = " ".join(currentGroup) - if speaker != "": - finalJAString = f"{speaker}: {finalJAString}" - else: - finalJAString = f"{finalJAString}" - - # [Passthrough 1] Pulling From File - if insertBool is False: - # Append to List and Clear Values - batch.append(finalJAString) - - # Translate Batch if Full - if len(batch) == BATCHSIZE or i >= len(linesList) - 1: - # Translate - response = translateGPT(batch, textHistory, True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - translatedBatch = response[0] - textHistory = translatedBatch[-10:] - - # Set Values - if len(batch) == len(translatedBatch): - i = batchStartIndex - insertBool = True - - # Mismatch - else: - pbar.write(f"Mismatch: {batchStartIndex} - {i}") - MISMATCH.append(batch) - batchStartIndex = i - batch.clear() - - multiLine = False - currentGroup = [] - - # [Passthrough 2] Setting Data - else: - # Get Text - translatedText = translatedBatch[0] - - # Remove added speaker and quotes - translatedText = re.sub(r"^.+?:\s", "", translatedText) - - # Textwrap - translatedText = translatedText.replace('"', '\\"') - translatedText = textwrap.fill(translatedText, width=WIDTH) - - # Set Data - if multiLine: - textList = translatedText.split("\n") - for t in textList: - translatedText = translatedText.replace(";", "") - translatedText = re.sub( - r"(s\[[0-9]+\]) = \"(.*)\"", - rf'\1 = "{t}"', - linesList[start], - ) - translatedText = translatedText.replace(";", "") - linesList[start] = translatedText - pbar.update(1) - start += 1 - multiLine = False - translatedText = translatedText.replace(";", "") - translatedBatch.pop(0) - else: - # Remove any textwrap - translatedText = translatedText.replace("\n", " ") - translatedText = re.sub( - r"(s\[[0-9]+\]) = \"(.*)\"", - rf'\1 = "{translatedText}"', - linesList[start], - ) - translatedText = translatedText.replace(";", "") - linesList[start] = translatedText - pbar.update(1) - translatedBatch.pop(0) - - # If Batch is empty. Move on. - if len(translatedBatch) == 0: - insertBool = False - batchStartIndex = i - pbar.update(1) - batch.clear() - - currentGroup = [] - else: - if insertBool is True: - pbar.update(1) - i += 1 - - return [linesList, tokens] - except Exception: - traceback.print_exc() - return [linesList, tokens] - - -def subVars(jaString): - jaString = jaString.replace("\u3000", " ") - - # Nested - count = 0 - nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) - nestedList = set(nestedList) - if len(nestedList) != 0: - for icon in nestedList: - jaString = jaString.replace(icon, "{Nested_" + str(count) + "}") - count += 1 - - # Icons - count = 0 - iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) - iconList = set(iconList) - if len(iconList) != 0: - for icon in iconList: - jaString = jaString.replace(icon, "{Ascii_" + str(count) + "}") - count += 1 - - # Colors - count = 0 - colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) - colorList = set(colorList) - if len(colorList) != 0: - for color in colorList: - jaString = jaString.replace(color, "{Color_" + str(count) + "}") - count += 1 - - # Names - count = 0 - nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) - nameList = set(nameList) - if len(nameList) != 0: - for name in nameList: - jaString = jaString.replace(name, "{Noun_" + str(count) + "}") - count += 1 - - # Variables - count = 0 - varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) - varList = set(varList) - if len(varList) != 0: - for var in varList: - jaString = jaString.replace(var, "{Var_" + str(count) + "}") - count += 1 - - # Formatting - count = 0 - formatList = re.findall(r"[\\]+[\w]+\[.+?\]", jaString) - formatList = set(formatList) - if len(formatList) != 0: - for var in formatList: - jaString = jaString.replace(var, "{FCode_" + str(count) + "}") - count += 1 - - # Put all lists in list and return - allList = [nestedList, iconList, colorList, nameList, varList, formatList] - return [jaString, allList] - - -def resubVars(translatedText, allList): - # Fix Spacing and ChatGPT Nonsense - matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) - if len(matchList) > 0: - for match in matchList: - text = match.strip() - translatedText = translatedText.replace(match, text) - - # Nested - count = 0 - if len(allList[0]) != 0: - for var in allList[0]: - translatedText = translatedText.replace("{Nested_" + str(count) + "}", var) - count += 1 - - # Icons - count = 0 - if len(allList[1]) != 0: - for var in allList[1]: - translatedText = translatedText.replace("{Ascii_" + str(count) + "}", var) - count += 1 - - # Colors - count = 0 - if len(allList[2]) != 0: - for var in allList[2]: - translatedText = translatedText.replace("{Color_" + str(count) + "}", var) - count += 1 - - # Names - count = 0 - if len(allList[3]) != 0: - for var in allList[3]: - translatedText = translatedText.replace("{Noun_" + str(count) + "}", var) - count += 1 - - # Vars - count = 0 - if len(allList[4]) != 0: - for var in allList[4]: - translatedText = translatedText.replace("{Var_" + str(count) + "}", var) - count += 1 - - # Formatting - count = 0 - if len(allList[5]) != 0: - for var in allList[5]: - translatedText = translatedText.replace("{FCode_" + str(count) + "}", var) - count += 1 - - return translatedText - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT): - characters = "Game Characters:\n\ -林つかさ (Tsukasa Hayashi) - Female\n\ -山田美兎 (Miyato Yamada) - Female\n\ -鈴木赤音 (Akane Suzuki) - Female\n\ -佐藤莉伊南 (Riina Satou) - Female\n\ -佐々木万梨美 (Marimi Sasaki) - Female\n\ -渡辺登樹子 (Tokiko Watanabe) - Female\n\ -桃乃夢 (Yume Momono) - Female\n\ -吉浦美雪 (Miyuki Yoshiura) - Female\n\ -三ツ門まあな (Maana Mitsukado) - Female\n\ -モリー・ボイド (Molly Boyd) - Female\n\ -オルガ・ブヤチッチ (Olga Buyachich) - Female\n\ -アッチャラー ギッティ (Atchara Gitti) - Female\n\ -" - - system = ( - PROMPT - if fullPromptFlag - else f"Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`" - ) - user = f"{subbedT}" - return characters, system, user - - -def translateText(characters, system, user, history): - # Prompt - msg = [{"role": "system", "content": system + characters}] - - # Characters - msg.append({"role": "system", "content": characters}) - - # History - if isinstance(history, list): - msg.extend([{"role": "assistant", "content": h} for h in history]) - else: - msg.append({"role": "assistant", "content": history}) - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0.1, - frequency_penalty=0.1, - model=MODEL, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - translatedText = resubVars(translatedText, varResponse[1]) - if "\n" in translatedText: - return [line for line in translatedText.split("\n") if line] - else: - return [line for line in translatedText.split("\\n") if line] - - -def extractTranslation(translatedTextList, is_list): - pattern = r"[\\]*`?(.*?)[\\]*?`?" - # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. - if is_list: - return [re.findall(pattern, line)[0][1] for line in translatedTextList if re.search(pattern, line)] - else: - matchList = re.findall(pattern, translatedTextList) - return matchList[0][1] if matchList else translatedTextList - - -def countTokens(characters, system, user, history): - inputTotalTokens = 0 - outputTotalTokens = 0 - enc = tiktoken.encoding_for_model("gpt-4") - - # Input - if isinstance(history, list): - for line in history: - inputTotalTokens += len(enc.encode(line)) - else: - inputTotalTokens += len(enc.encode(history)) - inputTotalTokens += len(enc.encode(system)) - inputTotalTokens += len(enc.encode(characters)) - inputTotalTokens += len(enc.encode(user)) - - # Output - outputTotalTokens += round(len(enc.encode(user)) * 3) - - return [inputTotalTokens, outputTotalTokens] - - -@retry(exceptions=Exception, tries=5, delay=5) -def translateGPT(text, history, fullPromptFlag): - totalTokens = [0, 0] - if isinstance(text, list): - tList = batchList(text, BATCHSIZE) - else: - tList = [text] - - for index, tItem in enumerate(tList): - # Before sending to translation, if we have a list of items, add the formatting - if isinstance(tItem, list): - payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) - payload = payload.replace("``", "`Placeholder Text`") - varResponse = subVars(payload) - subbedT = varResponse[0] - else: - varResponse = subVars(tItem) - subbedT = varResponse[0] - - # Things to Check before starting translation - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): - continue - - # Create Message - characters, system, user = createContext(fullPromptFlag, subbedT) - - # Calculate Estimate - if ESTIMATE: - estimate = countTokens(characters, system, user, history) - totalTokens[0] += estimate[0] - totalTokens[1] += estimate[1] - continue - - # Translating - response = translateText(characters, system, user, history) - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Formatting - translatedTextList = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedTextList, True) - tList[index] = extractedTranslations - if len(tItem) != len(translatedTextList): - mismatch = True # Just here so breakpoint can be set - history = extractedTranslations[-10:] # Update history if we have a list - else: - # Ensure we're passing a single string to extractTranslation - extractedTranslations = extractTranslation("\n".join(translatedTextList), False) - tList[index] = extractedTranslations - - # Combine if multilist - if isinstance(tList[0], list): - tList = [t for sublist in tList for t in sublist] - - # Return - if format == "json": - return [tList, totalTokens] - else: - return [tList[0], totalTokens] +# Libraries +import os +import re +import textwrap +import threading +import time +import traceback +import tiktoken +import openai +from pathlib import Path +from colorama import Fore +from dotenv import load_dotenv +from retry import retry +from tqdm import tqdm + +# Open AI +load_dotenv() +if os.getenv("api").replace(" ", "") != "": + openai.base_url = os.getenv("api") +openai.organization = os.getenv("org") +openai.api_key = os.getenv("key") + +# Globals +MODEL = os.getenv("model") +TIMEOUT = int(os.getenv("timeout")) +LANGUAGE = os.getenv("language").capitalize() +PROMPT = Path("prompt.txt").read_text(encoding="utf-8") +VOCAB = Path("vocab.txt").read_text(encoding="utf-8") +THREADS = int(os.getenv("threads")) +LOCK = threading.Lock() +WIDTH = int(os.getenv("width")) +LISTWIDTH = int(os.getenv("listWidth")) +NOTEWIDTH = 70 +MAXHISTORY = 10 +ESTIMATE = "" +TOKENS = [0, 0] +NAMESLIST = [] +NAMES = False # Output a list of all the character names found +BRFLAG = False # If the game uses
instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.01 + OUTPUTAPICOST = 0.03 + BATCHSIZE = 1 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleAlice(filename, estimate): + global ESTIMATE + totalTokens = [0, 0] + ESTIMATE = estimate + + if estimate: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + totalTokens[0] += translatedData[1][0] + totalTokens[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", totalTokens, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + else: + try: + with open("translated/" + filename, "w", encoding="UTF-8") as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + outFile.writelines(translatedData[0]) + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + totalTokens[0] += translatedData[1][0] + totalTokens[1] += translatedData[1][1] + except Exception: + traceback.print_exc() + return "Fail" + + return getResultString(["", totalTokens, None], end - start, "TOTAL") + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="UTF-8") as f: + translatedData = parseText(f, filename) + + return translatedData + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def parseText(data, filename): + # Get total for progress bar + linesList = data.readlines() + totalTokens = [0, 0] + totalLines = len(linesList) + global LOCK + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar: + pbar.desc = filename + pbar.total = totalLines + try: + result = translateLines(linesList, pbar) + totalTokens[0] += result[1][0] + totalTokens[1] += result[1][1] + except Exception as e: + traceback.print_exc() + return [linesList, totalTokens, e] + return [linesList, totalTokens, None] + + +# Grab scenario data from text file +def translateLines(linesList, pbar): + currentGroup = [] + batch = [] + textHistory = [] + tokens = [0, 0] + batchStartIndex = 0 + insertBool = False + multiLine = False + i = 0 + + try: + while i < len(linesList): + # Check if Proper Message + match = re.findall(r"s\[[0-9]+\] = \"(.*)\"", linesList[i]) + if len(match) > 0: + jaString = match[0] + + # Skip Files + if "/" in jaString: + i += 1 + continue + + ### Translate + # Remove any textwrap + jaString = re.sub(r"\\n", " ", jaString) + + # Grab Speaker + speakerMatch = re.findall(r"s\[[0-9]+\] = \"([^/]+)\"", linesList[i - 1]) + if len(speakerMatch) > 0: + # If there isn't any Japanese in the text just skip + if re.search(r"[一-龠]+|[ぁ-ゔ]+|[ァ-ヴー]+", jaString) and "_" not in speakerMatch[0]: + speaker = speakerMatch[0] + else: + speaker = "" + else: + speaker = "" + + # Grab rest of the messages + currentGroup.append(jaString) + + # Check if next line should be merged + if insertBool is True: + linesList[i] = re.sub(r"(s\[[0-9]+\]) = \"(.+)\"", r'\1 = ""', linesList[i]) + linesList[i] = linesList[i].replace(";", "") + start = i + while len(linesList) > i + 1 and re.search(r"s\[[0-9]+\] = \"\s+(.*)\"", linesList[i + 1]) != None: + multiLine = True + i += 1 + match = re.findall(r"s\[[0-9]+\] = \"\s+(.*)\"", linesList[i]) + currentGroup.append(match[0]) + if insertBool is True: + linesList[i] = re.sub(r"(s\[[0-9]+\]) = \"\s+(.+)\"", r'\1 = ""', linesList[i]) + linesList[i] = linesList[i].replace(";", "") + i += 1 + + # Combine Groups and Add Speaker + finalJAString = " ".join(currentGroup) + if speaker != "": + finalJAString = f"{speaker}: {finalJAString}" + else: + finalJAString = f"{finalJAString}" + + # [Passthrough 1] Pulling From File + if insertBool is False: + # Append to List and Clear Values + batch.append(finalJAString) + + # Translate Batch if Full + if len(batch) == BATCHSIZE or i >= len(linesList) - 1: + # Translate + response = translateGPT(batch, textHistory, True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedBatch = response[0] + textHistory = translatedBatch[-10:] + + # Set Values + if len(batch) == len(translatedBatch): + i = batchStartIndex + insertBool = True + + # Mismatch + else: + pbar.write(f"Mismatch: {batchStartIndex} - {i}") + MISMATCH.append(batch) + batchStartIndex = i + batch.clear() + + multiLine = False + currentGroup = [] + + # [Passthrough 2] Setting Data + else: + # Get Text + translatedText = translatedBatch[0] + + # Remove added speaker and quotes + translatedText = re.sub(r"^.+?:\s", "", translatedText) + + # Textwrap + translatedText = translatedText.replace('"', '\\"') + translatedText = textwrap.fill(translatedText, width=WIDTH) + + # Set Data + if multiLine: + textList = translatedText.split("\n") + for t in textList: + translatedText = translatedText.replace(";", "") + translatedText = re.sub( + r"(s\[[0-9]+\]) = \"(.*)\"", + rf'\1 = "{t}"', + linesList[start], + ) + translatedText = translatedText.replace(";", "") + linesList[start] = translatedText + pbar.update(1) + start += 1 + multiLine = False + translatedText = translatedText.replace(";", "") + translatedBatch.pop(0) + else: + # Remove any textwrap + translatedText = translatedText.replace("\n", " ") + translatedText = re.sub( + r"(s\[[0-9]+\]) = \"(.*)\"", + rf'\1 = "{translatedText}"', + linesList[start], + ) + translatedText = translatedText.replace(";", "") + linesList[start] = translatedText + pbar.update(1) + translatedBatch.pop(0) + + # If Batch is empty. Move on. + if len(translatedBatch) == 0: + insertBool = False + batchStartIndex = i + pbar.update(1) + batch.clear() + + currentGroup = [] + else: + if insertBool is True: + pbar.update(1) + i += 1 + + return [linesList, tokens] + except Exception: + traceback.print_exc() + return [linesList, tokens] + + +def subVars(jaString): + jaString = jaString.replace("\u3000", " ") + + # Nested + count = 0 + nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) + nestedList = set(nestedList) + if len(nestedList) != 0: + for icon in nestedList: + jaString = jaString.replace(icon, "{Nested_" + str(count) + "}") + count += 1 + + # Icons + count = 0 + iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) + iconList = set(iconList) + if len(iconList) != 0: + for icon in iconList: + jaString = jaString.replace(icon, "{Ascii_" + str(count) + "}") + count += 1 + + # Colors + count = 0 + colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) + colorList = set(colorList) + if len(colorList) != 0: + for color in colorList: + jaString = jaString.replace(color, "{Color_" + str(count) + "}") + count += 1 + + # Names + count = 0 + nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) + nameList = set(nameList) + if len(nameList) != 0: + for name in nameList: + jaString = jaString.replace(name, "{Noun_" + str(count) + "}") + count += 1 + + # Variables + count = 0 + varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) + varList = set(varList) + if len(varList) != 0: + for var in varList: + jaString = jaString.replace(var, "{Var_" + str(count) + "}") + count += 1 + + # Formatting + count = 0 + formatList = re.findall(r"[\\]+[\w]+\[.+?\]", jaString) + formatList = set(formatList) + if len(formatList) != 0: + for var in formatList: + jaString = jaString.replace(var, "{FCode_" + str(count) + "}") + count += 1 + + # Put all lists in list and return + allList = [nestedList, iconList, colorList, nameList, varList, formatList] + return [jaString, allList] + + +def resubVars(translatedText, allList): + # Fix Spacing and ChatGPT Nonsense + matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) + if len(matchList) > 0: + for match in matchList: + text = match.strip() + translatedText = translatedText.replace(match, text) + + # Nested + count = 0 + if len(allList[0]) != 0: + for var in allList[0]: + translatedText = translatedText.replace("{Nested_" + str(count) + "}", var) + count += 1 + + # Icons + count = 0 + if len(allList[1]) != 0: + for var in allList[1]: + translatedText = translatedText.replace("{Ascii_" + str(count) + "}", var) + count += 1 + + # Colors + count = 0 + if len(allList[2]) != 0: + for var in allList[2]: + translatedText = translatedText.replace("{Color_" + str(count) + "}", var) + count += 1 + + # Names + count = 0 + if len(allList[3]) != 0: + for var in allList[3]: + translatedText = translatedText.replace("{Noun_" + str(count) + "}", var) + count += 1 + + # Vars + count = 0 + if len(allList[4]) != 0: + for var in allList[4]: + translatedText = translatedText.replace("{Var_" + str(count) + "}", var) + count += 1 + + # Formatting + count = 0 + if len(allList[5]) != 0: + for var in allList[5]: + translatedText = translatedText.replace("{FCode_" + str(count) + "}", var) + count += 1 + + return translatedText + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT): + characters = "Game Characters:\n\ +林つかさ (Tsukasa Hayashi) - Female\n\ +山田美兎 (Miyato Yamada) - Female\n\ +鈴木赤音 (Akane Suzuki) - Female\n\ +佐藤莉伊南 (Riina Satou) - Female\n\ +佐々木万梨美 (Marimi Sasaki) - Female\n\ +渡辺登樹子 (Tokiko Watanabe) - Female\n\ +桃乃夢 (Yume Momono) - Female\n\ +吉浦美雪 (Miyuki Yoshiura) - Female\n\ +三ツ門まあな (Maana Mitsukado) - Female\n\ +モリー・ボイド (Molly Boyd) - Female\n\ +オルガ・ブヤチッチ (Olga Buyachich) - Female\n\ +アッチャラー ギッティ (Atchara Gitti) - Female\n\ +" + + system = ( + PROMPT + if fullPromptFlag + else f"Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`" + ) + user = f"{subbedT}" + return characters, system, user + + +def translateText(characters, system, user, history): + # Prompt + msg = [{"role": "system", "content": system + characters}] + + # Characters + msg.append({"role": "system", "content": characters}) + + # History + if isinstance(history, list): + msg.extend([{"role": "assistant", "content": h} for h in history]) + else: + msg.append({"role": "assistant", "content": history}) + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0.1, + frequency_penalty=0.1, + model=MODEL, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + translatedText = resubVars(translatedText, varResponse[1]) + if "\n" in translatedText: + return [line for line in translatedText.split("\n") if line] + else: + return [line for line in translatedText.split("\\n") if line] + + +def extractTranslation(translatedTextList, is_list): + pattern = r"[\\]*`?(.*?)[\\]*?`?" + # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. + if is_list: + return [re.findall(pattern, line)[0][1] for line in translatedTextList if re.search(pattern, line)] + else: + matchList = re.findall(pattern, translatedTextList) + return matchList[0][1] if matchList else translatedTextList + + +def countTokens(characters, system, user, history): + inputTotalTokens = 0 + outputTotalTokens = 0 + enc = tiktoken.encoding_for_model("gpt-4") + + # Input + if isinstance(history, list): + for line in history: + inputTotalTokens += len(enc.encode(line)) + else: + inputTotalTokens += len(enc.encode(history)) + inputTotalTokens += len(enc.encode(system)) + inputTotalTokens += len(enc.encode(characters)) + inputTotalTokens += len(enc.encode(user)) + + # Output + outputTotalTokens += round(len(enc.encode(user)) * 3) + + return [inputTotalTokens, outputTotalTokens] + + +@retry(exceptions=Exception, tries=5, delay=5) +def translateGPT(text, history, fullPromptFlag): + totalTokens = [0, 0] + if isinstance(text, list): + tList = batchList(text, BATCHSIZE) + else: + tList = [text] + + for index, tItem in enumerate(tList): + # Before sending to translation, if we have a list of items, add the formatting + if isinstance(tItem, list): + payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) + payload = payload.replace("``", "`Placeholder Text`") + varResponse = subVars(payload) + subbedT = varResponse[0] + else: + varResponse = subVars(tItem) + subbedT = varResponse[0] + + # Things to Check before starting translation + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): + continue + + # Create Message + characters, system, user = createContext(fullPromptFlag, subbedT) + + # Calculate Estimate + if ESTIMATE: + estimate = countTokens(characters, system, user, history) + totalTokens[0] += estimate[0] + totalTokens[1] += estimate[1] + continue + + # Translating + response = translateText(characters, system, user, history) + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Formatting + translatedTextList = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedTextList, True) + tList[index] = extractedTranslations + if len(tItem) != len(translatedTextList): + mismatch = True # Just here so breakpoint can be set + history = extractedTranslations[-10:] # Update history if we have a list + else: + # Ensure we're passing a single string to extractTranslation + extractedTranslations = extractTranslation("\n".join(translatedTextList), False) + tList[index] = extractedTranslations + + # Combine if multilist + if isinstance(tList[0], list): + tList = [t for sublist in tList for t in sublist] + + # Return + if format == "json": + return [tList, totalTokens] + else: + return [tList[0], totalTokens] diff --git a/modules/anim.py b/modules/anim.py index 971c5de..186695c 100644 --- a/modules/anim.py +++ b/modules/anim.py @@ -1,570 +1,571 @@ -# Libraries -import json -import os -import re -import textwrap -import threading -import time -import traceback -import tiktoken -import openai -from pathlib import Path -from colorama import Fore -from dotenv import load_dotenv -from retry import retry -from tqdm import tqdm - -# Open AI -load_dotenv() -if os.getenv("api").replace(" ", "") != "": - openai.base_url = os.getenv("api") -openai.organization = os.getenv("org") -openai.api_key = os.getenv("key") - -# Globals -MODEL = os.getenv("model") -TIMEOUT = int(os.getenv("timeout")) -LANGUAGE = os.getenv("language").capitalize() -PROMPT = Path("prompt.txt").read_text(encoding="utf-8") -VOCAB = Path("vocab.txt").read_text(encoding="utf-8") -THREADS = int(os.getenv("threads")) -LOCK = threading.Lock() -WIDTH = int(os.getenv("width")) -LISTWIDTH = int(os.getenv("listWidth")) -NOTEWIDTH = 70 -MAXHISTORY = 10 -ESTIMATE = "" -TOKENS = [0, 0] -NAMESLIST = [] -NAMES = False # Output a list of all the character names found -BRFLAG = False # If the game uses
instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.01 - OUTPUTAPICOST = 0.03 - BATCHSIZE = 50 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleAnim(filename, estimate): - global ESTIMATE - totalTokens = [0, 0] - ESTIMATE = estimate - - if estimate: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - totalTokens[0] += translatedData[1][0] - totalTokens[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", totalTokens, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - else: - try: - with open("translated/" + filename, "w", encoding="UTF-8") as outFile: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4) - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - totalTokens[0] += translatedData[1][0] - totalTokens[1] += translatedData[1][1] - except Exception: - return "Fail" - - return getResultString(["", totalTokens, None], end - start, "TOTAL") - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="UTF-8-sig") as f: - data = json.load(f) - - # Map Files - if ".json" in filename: - translatedData = parseJSON(data, filename) - - else: - raise NameError(filename + " Not Supported") - - return translatedData - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def parseJSON(data, filename): - keys = list(data.keys()) - batches = [keys[i : i + BATCHSIZE] for i in range(0, len(keys), BATCHSIZE)] - totalTokens = [0, 0] - totalLines = 0 - totalLines = len(batches) - global LOCK - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar: - pbar.desc = filename - pbar.total = totalLines - try: - result = translateJSON(batches, data, pbar) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateJSON(keys, data, pbar): - translatedBatch = [] - textHistory = [] - tokens = [0, 0] - - for batch in keys: - # Save Batch - originalBatch = batch.copy() - - # If there isn't any Japanese in the text just skip - needTL = False - for i in range(len(batch)): - t = data[batch[i]] - if re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", t) or t == "": - needTL = True - if needTL is False and IGNORETLTEXT is True: - pbar.update(1) - continue - - # Remove any textwrap and Furigana - for i in range(len(batch)): - if FIXTEXTWRAP == True: - # Textwrap - data[originalBatch[i]] = data[originalBatch[i]].replace("@b", " ") - - # Furigana - rcodeMatch = re.findall(r"(@\[(.+?):.+?\])", batch[i]) - if len(rcodeMatch) > 0: - for match in rcodeMatch: - batch[i] = batch[i].replace(match[0], match[1]) - - # Translate - if needTL is True: - response = translateGPT(batch, textHistory, True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - translatedBatch = response[0] - else: - for i in range(len(originalBatch)): - translatedBatch.append(data[originalBatch[i]]) - - # Format and Set Text - if len(batch) == len(translatedBatch): - for i in range(len(translatedBatch)): - # Remove added speaker - translatedText = translatedBatch[i] - translatedText = re.sub(r"^.+?\s\|\s?", "", translatedText) - - # Textwrap - if "@n" in translatedText: - match = re.search(r".*@n(.*)", translatedText) - if match != None: - tlText = match.group(1) - tlText = textwrap.fill(tlText, width=WIDTH) - tlText = tlText.replace("\n", "@b") - translatedText = translatedText.replace(match.group(1), tlText) - - elif "@b" not in translatedText: - translatedText = textwrap.fill(translatedText, width=WIDTH) - translatedText = translatedText.replace("\n", "@b") - - # Set Data - data[originalBatch[i]] = translatedText - textHistory = translatedBatch - translatedBatch.clear() - # Mismatch, Skip Batch - else: - MISMATCH.append(batch) - pbar.update(1) - continue - pbar.update(1) - - return tokens - - -def subVars(jaString): - jaString = jaString.replace("\u3000", " ") - - # Nested - count = 0 - nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) - nestedList = set(nestedList) - if len(nestedList) != 0: - for icon in nestedList: - jaString = jaString.replace(icon, "[Nested_" + str(count) + "]") - count += 1 - - # Icons - count = 0 - iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) - iconList = set(iconList) - if len(iconList) != 0: - for icon in iconList: - jaString = jaString.replace(icon, "[Ascii_" + str(count) + "]") - count += 1 - - # Colors - count = 0 - colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) - colorList = set(colorList) - if len(colorList) != 0: - for color in colorList: - jaString = jaString.replace(color, "[Color_" + str(count) + "]") - count += 1 - - # Names - count = 0 - nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) - nameList = set(nameList) - if len(nameList) != 0: - for name in nameList: - jaString = jaString.replace(name, "[Noun_" + str(count) + "]") - count += 1 - - # Variables - count = 0 - varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) - varList = set(varList) - if len(varList) != 0: - for var in varList: - jaString = jaString.replace(var, "[Var_" + str(count) + "]") - count += 1 - - # Formatting - count = 0 - formatList = re.findall(r"[\\]+[\w]+\[[a-zA-Z0-9\\\[\]\_,\s-]+\]", jaString) - formatList = set(formatList) - if len(formatList) != 0: - for var in formatList: - jaString = jaString.replace(var, "[FCode_" + str(count) + "]") - count += 1 - - # Put all lists in list and return - allList = [nestedList, iconList, colorList, nameList, varList, formatList] - return [jaString, allList] - - -def resubVars(translatedText, allList): - # Fix Spacing and ChatGPT Nonsense - matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) - if len(matchList) > 0: - for match in matchList: - text = match.strip() - translatedText = translatedText.replace(match, text) - - # Nested - count = 0 - if len(allList[0]) != 0: - for var in allList[0]: - translatedText = translatedText.replace("[Nested_" + str(count) + "]", var) - count += 1 - - # Icons - count = 0 - if len(allList[1]) != 0: - for var in allList[1]: - translatedText = translatedText.replace("[Ascii_" + str(count) + "]", var) - count += 1 - - # Colors - count = 0 - if len(allList[2]) != 0: - for var in allList[2]: - translatedText = translatedText.replace("[Color_" + str(count) + "]", var) - count += 1 - - # Names - count = 0 - if len(allList[3]) != 0: - for var in allList[3]: - translatedText = translatedText.replace("[Noun_" + str(count) + "]", var) - count += 1 - - # Vars - count = 0 - if len(allList[4]) != 0: - for var in allList[4]: - translatedText = translatedText.replace("[Var_" + str(count) + "]", var) - count += 1 - - # Formatting - count = 0 - if len(allList[5]) != 0: - for var in allList[5]: - translatedText = translatedText.replace("[FCode_" + str(count) + "]", var) - count += 1 - - return translatedText - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT): - characters = "Game Characters:\n\ -達也 (Tatsuya) - Male\n\ -香織 (Kaori) - Female\n\ -岩瀬 (Iwase)\n\ -万蔵 (Manzou) - Male\n\ -結奈 (Yuuna) - Female\n\ -茅部 (Kayabe)\n\ -" - - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - user = f"{subbedT}" - return characters, system, user - - -def translateText(characters, system, user, history, penalty): - # Prompt - msg = [{"role": "system", "content": system + characters}] - - # Characters - msg.append({"role": "system", "content": characters}) - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=MODEL, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "Placeholder Text": "", - "é": "e", - "—": "-", - "ū": "u", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - translatedText = resubVars(translatedText, varResponse[1]) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - pattern = r"`?<[Ll]ine\d+>([\\]*.*?[\\]*?)<\/?[Ll]ine\d+>`?" - # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. - if is_list: - matchList = re.findall(pattern, translatedTextList) - return matchList - else: - matchList = re.findall(pattern, translatedTextList) - return matchList[0][0] if matchList else translatedTextList - - -def countTokens(characters, system, user, history): - inputTotalTokens = 0 - outputTotalTokens = 0 - enc = tiktoken.encoding_for_model("gpt-4") - - # Input - if isinstance(history, list): - for line in history: - inputTotalTokens += len(enc.encode(line)) - else: - inputTotalTokens += len(enc.encode(history)) - inputTotalTokens += len(enc.encode(system)) - inputTotalTokens += len(enc.encode(characters)) - inputTotalTokens += len(enc.encode(user)) - - # Output - outputTotalTokens += round(len(enc.encode(user)) * 3) - - return [inputTotalTokens, outputTotalTokens] - - -@retry(exceptions=Exception, tries=5, delay=5) -def translateGPT(text, history, fullPromptFlag): - mismatch = False - totalTokens = [0, 0] - if isinstance(text, list): - tList = batchList(text, BATCHSIZE) - else: - tList = [text] - - for index, tItem in enumerate(tList): - # Before sending to translation, if we have a list of items, add the formatting - if isinstance(tItem, list): - payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) - payload = re.sub(r"(<)(\/Line\d+>)", r"\1>Placeholder Text<\3", payload) - varResponse = subVars(payload) - subbedT = varResponse[0] - else: - varResponse = subVars(tItem) - subbedT = varResponse[0] - - # Things to Check before starting translation - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): - continue - - # Create Message - characters, system, user = createContext(fullPromptFlag, subbedT) - - # Calculate Estimate - if ESTIMATE: - estimate = countTokens(characters, system, user, history) - totalTokens[0] += estimate[0] - totalTokens[1] += estimate[1] - continue - - # Translating - response = translateText(characters, system, user, history, 0.02) - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Formatting - translatedText = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedText, True) - tList[index] = extractedTranslations - if len(tItem) != len(extractedTranslations): - # Mismatch. Try Again - response = translateText(characters, system, user, history, 0.1) - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Formatting - translatedText = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedText, True) - tList[index] = extractedTranslations - if len(tItem) != len(extractedTranslations): - mismatch = True # Just here for breakpoint - - # Create History - if not mismatch: - history = extractedTranslations[-10:] # Update history if we have a list - else: - history = text[-10:] - else: - # Ensure we're passing a single string to extractTranslation - extractedTranslations = extractTranslation(translatedText, False) - tList[index] = extractedTranslations - - # Combine if multilist - if isinstance(tList[0], list): - tList = [t for sublist in tList for t in sublist] - - # Return - if format == "json": - return [tList, totalTokens] - else: - return [tList[0], totalTokens] +# Libraries +import json +import os +import re +import textwrap +import threading +import time +import traceback +import tiktoken +import openai +from pathlib import Path +from colorama import Fore +from dotenv import load_dotenv +from retry import retry +from tqdm import tqdm + +# Open AI +load_dotenv() +if os.getenv("api").replace(" ", "") != "": + openai.base_url = os.getenv("api") +openai.organization = os.getenv("org") +openai.api_key = os.getenv("key") + +# Globals +MODEL = os.getenv("model") +TIMEOUT = int(os.getenv("timeout")) +LANGUAGE = os.getenv("language").capitalize() +PROMPT = Path("prompt.txt").read_text(encoding="utf-8") +VOCAB = Path("vocab.txt").read_text(encoding="utf-8") +THREADS = int(os.getenv("threads")) +LOCK = threading.Lock() +WIDTH = int(os.getenv("width")) +LISTWIDTH = int(os.getenv("listWidth")) +NOTEWIDTH = 70 +MAXHISTORY = 10 +ESTIMATE = "" +TOKENS = [0, 0] +NAMESLIST = [] +NAMES = False # Output a list of all the character names found +BRFLAG = False # If the game uses
instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.01 + OUTPUTAPICOST = 0.03 + BATCHSIZE = 50 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleAnim(filename, estimate): + global ESTIMATE + totalTokens = [0, 0] + ESTIMATE = estimate + + if estimate: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + totalTokens[0] += translatedData[1][0] + totalTokens[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", totalTokens, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + else: + try: + with open("translated/" + filename, "w", encoding="UTF-8") as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4) + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + totalTokens[0] += translatedData[1][0] + totalTokens[1] += translatedData[1][1] + except Exception: + return "Fail" + + return getResultString(["", totalTokens, None], end - start, "TOTAL") + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="UTF-8-sig") as f: + data = json.load(f) + + # Map Files + if ".json" in filename: + translatedData = parseJSON(data, filename) + + else: + raise NameError(filename + " Not Supported") + + return translatedData + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def parseJSON(data, filename): + keys = list(data.keys()) + batches = [keys[i : i + BATCHSIZE] for i in range(0, len(keys), BATCHSIZE)] + totalTokens = [0, 0] + totalLines = 0 + totalLines = len(batches) + global LOCK + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar: + pbar.desc = filename + pbar.total = totalLines + try: + result = translateJSON(batches, data, pbar) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateJSON(keys, data, pbar): + translatedBatch = [] + textHistory = [] + tokens = [0, 0] + + for batch in keys: + # Save Batch + originalBatch = batch.copy() + + # If there isn't any Japanese in the text just skip + needTL = False + for i in range(len(batch)): + t = data[batch[i]] + if re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", t) or t == "": + needTL = True + if needTL is False and IGNORETLTEXT is True: + pbar.update(1) + continue + + # Remove any textwrap and Furigana + for i in range(len(batch)): + if FIXTEXTWRAP == True: + # Textwrap + data[originalBatch[i]] = data[originalBatch[i]].replace("@b", " ") + + # Furigana + rcodeMatch = re.findall(r"(@\[(.+?):.+?\])", batch[i]) + if len(rcodeMatch) > 0: + for match in rcodeMatch: + batch[i] = batch[i].replace(match[0], match[1]) + + # Translate + if needTL is True: + response = translateGPT(batch, textHistory, True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedBatch = response[0] + else: + for i in range(len(originalBatch)): + translatedBatch.append(data[originalBatch[i]]) + + # Format and Set Text + if len(batch) == len(translatedBatch): + for i in range(len(translatedBatch)): + # Remove added speaker + translatedText = translatedBatch[i] + translatedText = re.sub(r"^.+?\s\|\s?", "", translatedText) + + # Textwrap + if "@n" in translatedText: + match = re.search(r".*@n(.*)", translatedText) + if match != None: + tlText = match.group(1) + tlText = textwrap.fill(tlText, width=WIDTH) + tlText = tlText.replace("\n", "@b") + translatedText = translatedText.replace(match.group(1), tlText) + + elif "@b" not in translatedText: + translatedText = textwrap.fill(translatedText, width=WIDTH) + translatedText = translatedText.replace("\n", "@b") + + # Set Data + data[originalBatch[i]] = translatedText + textHistory = translatedBatch + translatedBatch.clear() + # Mismatch, Skip Batch + else: + MISMATCH.append(batch) + pbar.update(1) + continue + pbar.update(1) + + return tokens + + +def subVars(jaString): + jaString = jaString.replace("\u3000", " ") + + # Nested + count = 0 + nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) + nestedList = set(nestedList) + if len(nestedList) != 0: + for icon in nestedList: + jaString = jaString.replace(icon, "[Nested_" + str(count) + "]") + count += 1 + + # Icons + count = 0 + iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) + iconList = set(iconList) + if len(iconList) != 0: + for icon in iconList: + jaString = jaString.replace(icon, "[Ascii_" + str(count) + "]") + count += 1 + + # Colors + count = 0 + colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) + colorList = set(colorList) + if len(colorList) != 0: + for color in colorList: + jaString = jaString.replace(color, "[Color_" + str(count) + "]") + count += 1 + + # Names + count = 0 + nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) + nameList = set(nameList) + if len(nameList) != 0: + for name in nameList: + jaString = jaString.replace(name, "[Noun_" + str(count) + "]") + count += 1 + + # Variables + count = 0 + varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) + varList = set(varList) + if len(varList) != 0: + for var in varList: + jaString = jaString.replace(var, "[Var_" + str(count) + "]") + count += 1 + + # Formatting + count = 0 + formatList = re.findall(r"[\\]+[\w]+\[[a-zA-Z0-9\\\[\]\_,\s-]+\]", jaString) + formatList = set(formatList) + if len(formatList) != 0: + for var in formatList: + jaString = jaString.replace(var, "[FCode_" + str(count) + "]") + count += 1 + + # Put all lists in list and return + allList = [nestedList, iconList, colorList, nameList, varList, formatList] + return [jaString, allList] + + +def resubVars(translatedText, allList): + # Fix Spacing and ChatGPT Nonsense + matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) + if len(matchList) > 0: + for match in matchList: + text = match.strip() + translatedText = translatedText.replace(match, text) + + # Nested + count = 0 + if len(allList[0]) != 0: + for var in allList[0]: + translatedText = translatedText.replace("[Nested_" + str(count) + "]", var) + count += 1 + + # Icons + count = 0 + if len(allList[1]) != 0: + for var in allList[1]: + translatedText = translatedText.replace("[Ascii_" + str(count) + "]", var) + count += 1 + + # Colors + count = 0 + if len(allList[2]) != 0: + for var in allList[2]: + translatedText = translatedText.replace("[Color_" + str(count) + "]", var) + count += 1 + + # Names + count = 0 + if len(allList[3]) != 0: + for var in allList[3]: + translatedText = translatedText.replace("[Noun_" + str(count) + "]", var) + count += 1 + + # Vars + count = 0 + if len(allList[4]) != 0: + for var in allList[4]: + translatedText = translatedText.replace("[Var_" + str(count) + "]", var) + count += 1 + + # Formatting + count = 0 + if len(allList[5]) != 0: + for var in allList[5]: + translatedText = translatedText.replace("[FCode_" + str(count) + "]", var) + count += 1 + + return translatedText + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT): + characters = "Game Characters:\n\ +達也 (Tatsuya) - Male\n\ +香織 (Kaori) - Female\n\ +岩瀬 (Iwase)\n\ +万蔵 (Manzou) - Male\n\ +結奈 (Yuuna) - Female\n\ +茅部 (Kayabe)\n\ +" + + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + user = f"{subbedT}" + return characters, system, user + + +def translateText(characters, system, user, history, penalty): + # Prompt + msg = [{"role": "system", "content": system + characters}] + + # Characters + msg.append({"role": "system", "content": characters}) + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=MODEL, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "Placeholder Text": "", + "é": "e", + "—": "-", + "ū": "u", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + translatedText = resubVars(translatedText, varResponse[1]) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + pattern = r"`?<[Ll]ine\d+>([\\]*.*?[\\]*?)<\/?[Ll]ine\d+>`?" + # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. + if is_list: + matchList = re.findall(pattern, translatedTextList) + return matchList + else: + matchList = re.findall(pattern, translatedTextList) + return matchList[0][0] if matchList else translatedTextList + + +def countTokens(characters, system, user, history): + inputTotalTokens = 0 + outputTotalTokens = 0 + enc = tiktoken.encoding_for_model("gpt-4") + + # Input + if isinstance(history, list): + for line in history: + inputTotalTokens += len(enc.encode(line)) + else: + inputTotalTokens += len(enc.encode(history)) + inputTotalTokens += len(enc.encode(system)) + inputTotalTokens += len(enc.encode(characters)) + inputTotalTokens += len(enc.encode(user)) + + # Output + outputTotalTokens += round(len(enc.encode(user)) * 3) + + return [inputTotalTokens, outputTotalTokens] + + +@retry(exceptions=Exception, tries=5, delay=5) +def translateGPT(text, history, fullPromptFlag): + mismatch = False + totalTokens = [0, 0] + if isinstance(text, list): + tList = batchList(text, BATCHSIZE) + else: + tList = [text] + + for index, tItem in enumerate(tList): + # Before sending to translation, if we have a list of items, add the formatting + if isinstance(tItem, list): + payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) + payload = re.sub(r"(<)(\/Line\d+>)", r"\1>Placeholder Text<\3", payload) + varResponse = subVars(payload) + subbedT = varResponse[0] + else: + varResponse = subVars(tItem) + subbedT = varResponse[0] + + # Things to Check before starting translation + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): + continue + + # Create Message + characters, system, user = createContext(fullPromptFlag, subbedT) + + # Calculate Estimate + if ESTIMATE: + estimate = countTokens(characters, system, user, history) + totalTokens[0] += estimate[0] + totalTokens[1] += estimate[1] + continue + + # Translating + response = translateText(characters, system, user, history, 0.02) + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Formatting + translatedText = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedText, True) + tList[index] = extractedTranslations + if len(tItem) != len(extractedTranslations): + # Mismatch. Try Again + response = translateText(characters, system, user, history, 0.1) + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Formatting + translatedText = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedText, True) + tList[index] = extractedTranslations + if len(tItem) != len(extractedTranslations): + mismatch = True # Just here for breakpoint + + # Create History + if not mismatch: + history = extractedTranslations[-10:] # Update history if we have a list + else: + history = text[-10:] + else: + # Ensure we're passing a single string to extractTranslation + extractedTranslations = extractTranslation(translatedText, False) + tList[index] = extractedTranslations + + # Combine if multilist + if isinstance(tList[0], list): + tList = [t for sublist in tList for t in sublist] + + # Return + if format == "json": + return [tList, totalTokens] + else: + return [tList[0], totalTokens] diff --git a/modules/csv.py b/modules/csv.py index a94f795..a1d15ef 100644 --- a/modules/csv.py +++ b/modules/csv.py @@ -1,701 +1,701 @@ -# Libraries -import json -import os -import re -import textwrap -import threading -import time -import traceback -import tiktoken -import openai -import csv -from pathlib import Path -from colorama import Fore -from dotenv import load_dotenv -from retry import retry -from tqdm import tqdm - -# Open AI -load_dotenv() -if os.getenv("api").replace(" ", "") != "": - openai.base_url = os.getenv("api") -openai.organization = os.getenv("org") -openai.api_key = os.getenv("key") - -# Globals -MODEL = os.getenv("model") -TIMEOUT = int(os.getenv("timeout")) -LANGUAGE = os.getenv("language").capitalize() -PROMPT = Path("prompt.txt").read_text(encoding="utf-8") -VOCAB = Path("vocab.txt").read_text(encoding="utf-8") -THREADS = int(os.getenv("threads")) -LOCK = threading.Lock() -WIDTH = int(os.getenv("width")) -LISTWIDTH = int(os.getenv("listWidth")) -NOTEWIDTH = int(os.getenv("noteWidth")) -MAXHISTORY = 10 -ESTIMATE = "" -TOKENS = [0, 0] -NAMESLIST = [] -NAMES = False # Output a list of all the character names found -BRFLAG = False # If the game uses
instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = True # Ignores all translated text. -MISMATCH = [] # Lists files that thdata a mismatch error (Length of GPT list response is wrong) -BRACKETNAMES = False - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 - FREQUENCY_PENALTY = 0.2 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 20 - FREQUENCY_PENALTY = 0.1 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False -PBAR = None -ENCODING = "cp932" - - -def handleCSV(filename, estimate): - global ESTIMATE, TOKENS - ESTIMATE = estimate - - if not ESTIMATE: - with open("translated/" + filename, "w+t", newline="", encoding=ENCODING) as writeFile: - # Translate - start = time.time() - translatedData = openFiles(filename, writeFile) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - else: - # Translate - start = time.time() - translatedData = openFilesEstimate(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - -def openFiles(filename, writeFile): - with open("files/" + filename, "r", encoding="cp932") as readFile, writeFile: - translatedData = parseCSV(readFile, writeFile, filename) - - return translatedData - - -def openFilesEstimate(filename): - with open("files/" + filename, "r", encoding="cp932") as readFile: - translatedData = parseCSV(readFile, "", filename) - - return translatedData - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] is None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def parseCSV(readFile, writeFile, filename): - totalTokens = [0, 0] - totalLines = 0 - global LOCK - - # Read from tmp files - if os.path.isfile("csv.tmp"): - with open("csv.tmp") as tmpFile: - format = tmpFile.readline() - else: - format = "" - - # Choices - while format not in ["1", "2", "3", "4"]: - format = input("\n\nSelect the CSV Format:\n\n1. Translator++\n2. Single\n3. Multiple\n4. Speaker&Text\n") - match format: - case "1": - format = "1" - case "2": - format = "2" - case "3": - format = "3" - case "4": - format = "4" - - # Write to file for later use - with open("csv.tmp", "w", encoding="utf-8") as tmpFile: - tmpFile.write(f"{format}") - - # Get total for progress bar - totalLines = len(readFile.readlines()) - readFile.seek(0) - - reader = csv.reader(readFile, delimiter=",") - if not ESTIMATE: - writer = csv.writer(writeFile, delimiter=",") - else: - writer = "" - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar: - pbar.desc = filename - pbar.total = totalLines - - # Grab All Rows - data = [] - for row in reader: - data.append(row) - - try: - response = translateCSV(data, pbar, writer, filename, None, format) - totalTokens[0] = response[0] - totalTokens[1] = response[1] - except Exception: - traceback.print_exc() - return [data, totalTokens, None] - - -def translateCSV(data, pbar, writer, filename, translatedList, format): - global LOCK, ESTIMATE, PBAR - PBAR = pbar - translatedText = "" - totalTokens = [0, 0] - i = 0 - stringList = [] - - try: - # Translate - while i < len(data): - match format: - # T++ Format: Source Text on column 1. TL Target on Column 2 - case "1": - # Get String - if i != 0: - if data[i][1] == "": - jaString = data[i][0] - else: - jaString = data[i][1] - - # Remove Textwrap - jaString = jaString.replace("\\n", " ") - - # Pass 1 - if not translatedList: - stringList.append(jaString) - - # Pass 2 - else: - # Grab and Pop - translatedText = translatedList[0] - translatedList.pop(0) - - # Add Wordwrap - translatedText = textwrap.fill(translatedText, WIDTH) - translatedText = translatedText.replace("\n", "\\n") - - # Set Data - data[i][1] = translatedText - - # Iterate - i += 1 - - # Target Format - case "2": - # Set Values - sourceColumn = 0 - targetColumn = 1 - - # Check if Translated - jaString = data[i][sourceColumn] - - # Remove Textwrap - jaString = jaString.replace("\\n", " ") - - # Pass 1 - if not translatedList: - stringList.append(jaString) - - # Pass 2 - else: - # Grab and Pop - translatedText = translatedList[0] - translatedList.pop(0) - - # Add Wordwrap - translatedText = textwrap.fill(translatedText, WIDTH) - translatedText = translatedText.replace("\n", "\\n") - - # Set Data - data[i][targetColumn] = translatedText - - # Iterate - i += 1 - - # In Place Format - case "3": - # Set columns to translate. Leave empty to translate all. - targetColumns = [] - - # False - Place translation in source column - # True - Place translation in next column - targetNextRow = True - - for j in range(len(data[i])): - if j not in targetColumns: - # Check if Translated - jaString = data[i][j] - - # Remove Textwrap - jaString = jaString.replace("\\n", " ") - - # Pass 1 - if not translatedList: - stringList.append(jaString) - - # Pass 2 - else: - # Grab and Pop - translatedText = translatedList[0] - translatedList.pop(0) - - # Add Wordwrap - translatedText = textwrap.fill(translatedText, WIDTH) - translatedText = translatedText.replace("\n", "\\n") - - # Set Data - if targetNextRow: - data[i][j + 1] = translatedText - else: - data[i][j] = translatedText - - # Iterate - i += 1 - - # Speaker & Text Format - case "4": - # Set columns to translate. Leave empty to translate all. - speakerColumn = 8 - textColumn = 20 - speaker = "" - - if len(data[i]) > textColumn and data[i][textColumn]: - # Speaker - if data[i][speakerColumn]: - speakerResponse = getSpeaker(data[i][speakerColumn]) - totalTokens[0] += speakerResponse[1][0] - totalTokens[1] += speakerResponse[1][1] - speaker = speakerResponse[0] - data[i][speakerColumn] = speaker - - # Get Text - jaString = data[i][textColumn] - - # Remove Textwrap - jaString = jaString.replace("\\n", " ") - - # Remove Furigana - jaString = re.sub(r"<(.*)=.*>", r"\1", jaString) - - # Pass 1 - if not translatedList: - # Append Speaker - if speaker: - jaString = f"[{speaker}]: {jaString}" - - # Append to List - stringList.append(jaString) - - # Pass 2 - else: - # Grab and Pop - translatedText = translatedList[0] - translatedList.pop(0) - - # Remove speaker - if speaker: - translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText) - - # Add Wordwrap - translatedText = textwrap.fill(translatedText, WIDTH) - translatedText = translatedText.replace("\n", "\\n") - - # Set Data - data[i][textColumn] = translatedText - - # Iterate - i += 1 - - # EOF - if len(stringList) > 0: - # Set Progress - pbar.total = len(stringList) - pbar.refresh() - - # Translate - response = translateGPT(stringList, "", True) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - translatedList = response[0] - - # Set Strings - if len(stringList) == len(translatedList): - translateCSV(data, pbar, writer, filename, translatedList, format) - - # Mismatch - else: - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - - # Write all Data - with LOCK: - if not ESTIMATE: - for row in data: - writer.writerow(row) - - except Exception: - traceback.print_exc() - - # Write all Data - with LOCK: - if not ESTIMATE: - for row in data: - writer.writerow(row) - return totalTokens - - return totalTokens - - -# Save some money and enter the character before translation -def getSpeaker(speaker): - match speaker: - case "ファイン": - return ["Fine", [0, 0]] - case "": - return ["", [0, 0]] - case _: - # Find Speaker - for i in range(len(NAMESLIST)): - if speaker == NAMESLIST[i][0]: - return [NAMESLIST[i][1], [0, 0]] - - # If there isn't any Japanese in the text just skip - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", speaker): - return [speaker, [0, 0]] - - # Translate and Store Speaker - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - True, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - response[0] = response[0].replace("Speaker: ", "") - - # Retry if name doesn't translate for some reason - if re.search(r"([a-zA-Z??])", response[0]) == None: - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - - speakerList = [speaker, response[0]] - NAMESLIST.append(speakerList) - return response - return [speaker, [0, 0]] - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT, format): - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - if format == "json": - user = f"```json\n{subbedT}\n```" - else: - user = subbedT - return system, user - - -def translateText(system, user, history, penalty, format, model=MODEL): - # Prompt - msg = [{"role": "system", "content": system}] - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Response Format - if format == "json": - responseFormat = {"type": "json_object"} - else: - responseFormat = {"type": "text"} - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=model, - response_format=responseFormat, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "「": '\\"', - "」": '\\"', - "- ": "-", - "—": "―", - "】": "]", - "【": "[", - "é": "e", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Remove Repeating Characters - pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") - translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - try: - translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) - translatedTextList = re.sub(r"(? instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = True # Ignores all translated text. +MISMATCH = [] # Lists files that thdata a mismatch error (Length of GPT list response is wrong) +BRACKETNAMES = False + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 + FREQUENCY_PENALTY = 0.2 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 20 + FREQUENCY_PENALTY = 0.1 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False +PBAR = None +ENCODING = "cp932" + + +def handleCSV(filename, estimate): + global ESTIMATE, TOKENS + ESTIMATE = estimate + + if not ESTIMATE: + with open("translated/" + filename, "w+t", newline="", encoding=ENCODING) as writeFile: + # Translate + start = time.time() + translatedData = openFiles(filename, writeFile) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + else: + # Translate + start = time.time() + translatedData = openFilesEstimate(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + +def openFiles(filename, writeFile): + with open("files/" + filename, "r", encoding="cp932") as readFile, writeFile: + translatedData = parseCSV(readFile, writeFile, filename) + + return translatedData + + +def openFilesEstimate(filename): + with open("files/" + filename, "r", encoding="cp932") as readFile: + translatedData = parseCSV(readFile, "", filename) + + return translatedData + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] is None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def parseCSV(readFile, writeFile, filename): + totalTokens = [0, 0] + totalLines = 0 + global LOCK + + # Read from tmp files + if os.path.isfile("csv.tmp"): + with open("csv.tmp") as tmpFile: + format = tmpFile.readline() + else: + format = "" + + # Choices + while format not in ["1", "2", "3", "4"]: + format = input("\n\nSelect the CSV Format:\n\n1. Translator++\n2. Single\n3. Multiple\n4. Speaker&Text\n") + match format: + case "1": + format = "1" + case "2": + format = "2" + case "3": + format = "3" + case "4": + format = "4" + + # Write to file for later use + with open("csv.tmp", "w", encoding="utf-8") as tmpFile: + tmpFile.write(f"{format}") + + # Get total for progress bar + totalLines = len(readFile.readlines()) + readFile.seek(0) + + reader = csv.reader(readFile, delimiter=",") + if not ESTIMATE: + writer = csv.writer(writeFile, delimiter=",") + else: + writer = "" + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar: + pbar.desc = filename + pbar.total = totalLines + + # Grab All Rows + data = [] + for row in reader: + data.append(row) + + try: + response = translateCSV(data, pbar, writer, filename, None, format) + totalTokens[0] = response[0] + totalTokens[1] = response[1] + except Exception: + traceback.print_exc() + return [data, totalTokens, None] + + +def translateCSV(data, pbar, writer, filename, translatedList, format): + global LOCK, ESTIMATE, PBAR + PBAR = pbar + translatedText = "" + totalTokens = [0, 0] + i = 0 + stringList = [] + + try: + # Translate + while i < len(data): + match format: + # T++ Format: Source Text on column 1. TL Target on Column 2 + case "1": + # Get String + if i != 0: + if data[i][1] == "": + jaString = data[i][0] + else: + jaString = data[i][1] + + # Remove Textwrap + jaString = jaString.replace("\\n", " ") + + # Pass 1 + if not translatedList: + stringList.append(jaString) + + # Pass 2 + else: + # Grab and Pop + translatedText = translatedList[0] + translatedList.pop(0) + + # Add Wordwrap + translatedText = textwrap.fill(translatedText, WIDTH) + translatedText = translatedText.replace("\n", "\\n") + + # Set Data + data[i][1] = translatedText + + # Iterate + i += 1 + + # Target Format + case "2": + # Set Values + sourceColumn = 0 + targetColumn = 1 + + # Check if Translated + jaString = data[i][sourceColumn] + + # Remove Textwrap + jaString = jaString.replace("\\n", " ") + + # Pass 1 + if not translatedList: + stringList.append(jaString) + + # Pass 2 + else: + # Grab and Pop + translatedText = translatedList[0] + translatedList.pop(0) + + # Add Wordwrap + translatedText = textwrap.fill(translatedText, WIDTH) + translatedText = translatedText.replace("\n", "\\n") + + # Set Data + data[i][targetColumn] = translatedText + + # Iterate + i += 1 + + # In Place Format + case "3": + # Set columns to translate. Leave empty to translate all. + targetColumns = [] + + # False - Place translation in source column + # True - Place translation in next column + targetNextRow = False + + for j in range(len(data[i])): + if j not in targetColumns: + # Check if Translated + jaString = data[i][j] + + # Remove Textwrap + jaString = jaString.replace("\\n", " ") + + # Pass 1 + if not translatedList: + stringList.append(jaString) + + # Pass 2 + else: + # Grab and Pop + translatedText = translatedList[0] + translatedList.pop(0) + + # Add Wordwrap + translatedText = textwrap.fill(translatedText, WIDTH) + translatedText = translatedText.replace("\n", "\\n") + + # Set Data + if targetNextRow: + data[i][j + 1] = translatedText + else: + data[i][j] = translatedText + + # Iterate + i += 1 + + # Speaker & Text Format + case "4": + # Set columns to translate. Leave empty to translate all. + speakerColumn = 8 + textColumn = 20 + speaker = "" + + if len(data[i]) > textColumn and data[i][textColumn]: + # Speaker + if data[i][speakerColumn]: + speakerResponse = getSpeaker(data[i][speakerColumn]) + totalTokens[0] += speakerResponse[1][0] + totalTokens[1] += speakerResponse[1][1] + speaker = speakerResponse[0] + data[i][speakerColumn] = speaker + + # Get Text + jaString = data[i][textColumn] + + # Remove Textwrap + jaString = jaString.replace("\\n", " ") + + # Remove Furigana + jaString = re.sub(r"<(.*)=.*>", r"\1", jaString) + + # Pass 1 + if not translatedList: + # Append Speaker + if speaker: + jaString = f"[{speaker}]: {jaString}" + + # Append to List + stringList.append(jaString) + + # Pass 2 + else: + # Grab and Pop + translatedText = translatedList[0] + translatedList.pop(0) + + # Remove speaker + if speaker: + translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText) + + # Add Wordwrap + translatedText = textwrap.fill(translatedText, WIDTH) + translatedText = translatedText.replace("\n", "\\n") + + # Set Data + data[i][textColumn] = translatedText + + # Iterate + i += 1 + + # EOF + if len(stringList) > 0: + # Set Progress + pbar.total = len(stringList) + pbar.refresh() + + # Translate + response = translateGPT(stringList, "", True) + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + translatedList = response[0] + + # Set Strings + if len(stringList) == len(translatedList): + translateCSV(data, pbar, writer, filename, translatedList, format) + + # Mismatch + else: + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + + # Write all Data + with LOCK: + if not ESTIMATE: + for row in data: + writer.writerow(row) + + except Exception: + traceback.print_exc() + + # Write all Data + with LOCK: + if not ESTIMATE: + for row in data: + writer.writerow(row) + return totalTokens + + return totalTokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + match speaker: + case "ファイン": + return ["Fine", [0, 0]] + case "": + return ["", [0, 0]] + case _: + # Find Speaker + for i in range(len(NAMESLIST)): + if speaker == NAMESLIST[i][0]: + return [NAMESLIST[i][1], [0, 0]] + + # If there isn't any Japanese in the text just skip + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", speaker): + return [speaker, [0, 0]] + + # Translate and Store Speaker + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + True, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + response[0] = response[0].replace("Speaker: ", "") + + # Retry if name doesn't translate for some reason + if re.search(r"([a-zA-Z??])", response[0]) == None: + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + False, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + + speakerList = [speaker, response[0]] + NAMESLIST.append(speakerList) + return response + return [speaker, [0, 0]] + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT, format): + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + if format == "json": + user = f"```json\n{subbedT}\n```" + else: + user = subbedT + return system, user + + +def translateText(system, user, history, penalty, format, model=MODEL): + # Prompt + msg = [{"role": "system", "content": system}] + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Response Format + if format == "json": + responseFormat = {"type": "json_object"} + else: + responseFormat = {"type": "text"} + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=model, + response_format=responseFormat, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "「": '\\"', + "」": '\\"', + "- ": "-", + "—": "―", + "】": "]", + "【": "[", + "é": "e", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Remove Repeating Characters + pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") + translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + try: + translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) + translatedTextList = re.sub(r"(? instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False -PBAR = None - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 40 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleEushully(filename, estimate): - global ESTIMATE - ESTIMATE = estimate - - if ESTIMATE: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - else: - try: - with open("translated/" + filename, "w", encoding="utf-8", errors="ignore") as outFile: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - outFile.writelines(translatedData[0]) - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - except Exception: - traceback.print_exc() - return "Fail" - - return getResultString(["", TOKENS, None], end - start, "TOTAL") - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="utf-8") as readFile: - translatedData = parseRegex(readFile, filename) - - # Delete lines marked for deletion - finalData = [] - for line in translatedData[0]: - if line != "\\d\n": - finalData.append(line) - translatedData[0] = finalData - - return translatedData - - -def parseRegex(readFile, filename): - totalTokens = [0, 0] - - # Read File into data - data = readFile.readlines() - - # Create Progress Bar - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - - try: - result = translateEushully(data, pbar, filename, []) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateEushully(data, pbar, filename, translatedList): - stringList = [] - currentGroup = [] - tokens = [0, 0] - speaker = "" - voice = False - global LOCK, ESTIMATE, PBAR - i = 0 - - while i < len(data): - voice = False - # Speaker - if "mov (global-int 46e2)" in data[i]: - # Get Speaker - speaker = re.search(r"mov \(global-int 46e2\)\s(.+)", data[i]).group(1) - response = getSpeaker(speaker) - speaker = response[0] - tokens[0] += response[1][0] - tokens[1] += response[1][1] - i += 1 - - # Show Text - if any(x in data[i] for x in ["show-text"]): - # Lines - regex = r'(.*?)"(.*)"' - match = re.search(regex, data[i]) - # Grab Strings - if match != None and match.group(2) != "": - originalString = match.group(2) - jaString = match.group(2) - currentGroup = [jaString] - while "end-text-line" in data[i + 1] and any(x in data[i + 2] for x in ["show-text"]): - match = re.search(regex, data[i + 2]) - if match != None: - currentGroup.append(match.group(2)) - if translatedList == []: - del data[i] - del data[i] - jaString = " ".join(currentGroup) - - # Pass 1 - if translatedList == []: - # Add String - if speaker: - stringList.append(f"[{speaker}]: {jaString.strip()}") - else: - stringList.append(jaString.strip()) - - # Pass 2 - else: - # Get Text - if translatedList: - # Grab and Pop - translatedText = translatedList[0] - translatedList.pop(0) - - # Set to None if empty list - if len(translatedList) <= 0: - translatedList = None - - # Replace Quotes - translatedText = translatedText.replace('"', "'") - - # Remove speaker - if speaker != "": - translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText) - - # Textwrap - translatedText = textwrap.fill(translatedText, width=WIDTH) - translatedTextList = translatedText.split("\n") - - # Set Data - if len(translatedTextList) > 1: - for j in range(len(translatedTextList)): - if any(x in data[i] for x in ["show-text", "set-string", "concat"]): - del data[i] - data.insert(i, f'{match.group(1)}"{translatedTextList[j]}"\n') - i += 1 - if "end-text-line" not in data[i]: - data.insert(i, "end-text-line 0\n") - i += 1 - else: - data[i] = f'{match.group(1)}"{translatedTextList[0]}"\n' - speaker = "" - i += 1 - - # Nothing relevant. Skip Line. - else: - i += 1 - - # Set String - elif "set-string" in data[i]: - # Lines - regex = r'(.*?)"(.*)"' - match = re.search(regex, data[i]) - # Grab Strings - if match != None and match.group(2) != "": - originalString = match.group(2) - jaString = match.group(2) - currentGroup = [jaString] - - # Remove Textwrap - jaString = jaString.replace("\\n", " ") - - # Pass 1 - if translatedList == []: - # Add String - stringList.append(jaString.strip()) - - # Pass 2 - else: - # Get Text - if translatedList: - # Grab and Pop - translatedText = translatedList[0] - translatedList.pop(0) - - # Set to None if empty list - if len(translatedList) <= 0: - translatedList = None - - # Replace Quotes - translatedText = translatedText.replace('"', "'") - - # Textwrap - translatedText = textwrap.fill(translatedText, width=LISTWIDTH) - translatedText = translatedText.replace("\n", "\\n") - - # Set Data - data[i] = data[i].replace(originalString, translatedText) - speaker = "" - i += 1 - - # Nothing relevant. Skip Line. - else: - i += 1 - else: - i += 1 - - # EOF - if len(stringList) > 0: - # Set Progress - pbar.total = len(stringList) - pbar.refresh() - - # Translate - PBAR = pbar - response = translateGPT(stringList, "", True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - translatedList = response[0] - - # Set Strings - if len(stringList) == len(translatedList): - translateEushully(data, pbar, filename, translatedList) - - # Mismatch - else: - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - return tokens - - -# Save some money and enter the character before translation -def getSpeaker(speaker): - match speaker: - case "1": - return ["Klaus", [0, 0]] - case "2": - return ["Helmina", [0, 0]] - case "3": - return ["Juliana", [0, 0]] - case "4": - return ["Reginia", [0, 0]] - case "5": - return ["Luciel", [0, 0]] - case "6": - return ["Mavislaine", [0, 0]] - case "7": - return ["Cerouge", [0, 0]] - case "8": - return ["Maize", [0, 0]] - case "9": - return ["Elvire", [0, 0]] - case "a": - return ["Beatrice", [0, 0]] - case "295": - return ["Orc", [0, 0]] - case "232": - return ["Archangel", [0, 0]] - case "238": - return ["False Juliana", [0, 0]] - case "239": - return ["False Regina", [0, 0]] - case "23a": - return ["False Luciel", [0, 0]] - case "23d": - return ["False Mavislaine", [0, 0]] - case "cb": - return ["Olga Niza Kite", [0, 0]] - case "c9": - return ["Demon Beast Lupus", [0, 0]] - case "ca": - return ["Evelinael", [0, 0]] - case "10": - return ["Eukleia", [0, 0]] - case "15": - return ["Lily", [0, 0]] - case "16": - return ["Kupuko", [0, 0]] - case "b": - return ["Ramiel", [0, 0]] - case "c": - return ["Henriette", [0, 0]] - case "d": - return ["Camilla", [0, 0]] - case "cc": - return ["Gogonaua", [0, 0]] - case "65": - return ["Demon Lord Reyvalois", [0, 0]] - case "d0": - return ["Demon Ranwald", [0, 0]] - case "205": - return ["Vanqueor", [0, 0]] - case "66": - return ["Angel Martina", [0, 0]] - case "21f": - return ["Hiten Demon", [0, 0]] - case "d2": - return ["Lena Eli", [0, 0]] - case _: - return ["Unknown", [0, 0]] - - -def subVars(jaString): - jaString = jaString.replace("\u3000", " ") - - # Nested - count = 0 - nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) - nestedList = set(nestedList) - if len(nestedList) != 0: - for icon in nestedList: - jaString = jaString.replace(icon, "[Nested_" + str(count) + "]") - count += 1 - - # Icons - count = 0 - iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) - iconList = set(iconList) - if len(iconList) != 0: - for icon in iconList: - jaString = jaString.replace(icon, "[Ascii_" + str(count) + "]") - count += 1 - - # Colors - count = 0 - colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) - colorList = set(colorList) - if len(colorList) != 0: - for color in colorList: - jaString = jaString.replace(color, "[Color_" + str(count) + "]") - count += 1 - - # Names - count = 0 - nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) - nameList = set(nameList) - if len(nameList) != 0: - for name in nameList: - jaString = jaString.replace(name, "[Noun_" + str(count) + "]") - count += 1 - - # Variables - count = 0 - varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) - varList = set(varList) - if len(varList) != 0: - for var in varList: - jaString = jaString.replace(var, "[Var_" + str(count) + "]") - count += 1 - - # Formatting - count = 0 - formatList = re.findall(r"[\\]+[\w]+\[[a-zA-Z0-9\\\[\]\_,\s-]+\]", jaString) - formatList = set(formatList) - if len(formatList) != 0: - for var in formatList: - jaString = jaString.replace(var, "[FCode_" + str(count) + "]") - count += 1 - - # Put all lists in list and return - allList = [nestedList, iconList, colorList, nameList, varList, formatList] - return [jaString, allList] - - -def resubVars(translatedText, allList): - # Fix Spacing and ChatGPT Nonsense - matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) - if len(matchList) > 0: - for match in matchList: - text = match.strip() - translatedText = translatedText.replace(match, text) - - # Nested - count = 0 - if len(allList[0]) != 0: - for var in allList[0]: - translatedText = translatedText.replace("[Nested_" + str(count) + "]", var) - count += 1 - - # Icons - count = 0 - if len(allList[1]) != 0: - for var in allList[1]: - translatedText = translatedText.replace("[Ascii_" + str(count) + "]", var) - count += 1 - - # Colors - count = 0 - if len(allList[2]) != 0: - for var in allList[2]: - translatedText = translatedText.replace("[Color_" + str(count) + "]", var) - count += 1 - - # Names - count = 0 - if len(allList[3]) != 0: - for var in allList[3]: - translatedText = translatedText.replace("[Noun_" + str(count) + "]", var) - count += 1 - - # Vars - count = 0 - if len(allList[4]) != 0: - for var in allList[4]: - translatedText = translatedText.replace("[Var_" + str(count) + "]", var) - count += 1 - - # Formatting - count = 0 - if len(allList[5]) != 0: - for var in allList[5]: - translatedText = translatedText.replace("[FCode_" + str(count) + "]", var) - count += 1 - - return translatedText - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT): - characters = "Game Characters:\n\ -グレイス (Grace) - Female\n\ -" - - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - user = f"{subbedT}" - return characters, system, user - - -def translateText(characters, system, user, history, penalty): - # Prompt - msg = [{"role": "system", "content": system + characters}] - - # Characters - msg.append({"role": "system", "content": characters}) - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=MODEL, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "< ": "<", - "": ">", - "「": '"', - "」": '"', - "Placeholder Text": "", - "- chan": "-chan", - "- kun": "-kun", - "- san": "-san", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - translatedText = resubVars(translatedText, varResponse[1]) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - pattern = r"`?<[Ll]ine\d+>([\\]*.*?[\\]*?)<\/?[Ll]ine\d+>`?" - # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. - if is_list: - matchList = re.findall(pattern, translatedTextList) - return matchList - else: - matchList = re.findall(pattern, translatedTextList) - return matchList[0][0] if matchList else translatedTextList - - -def countTokens(characters, system, user, history): - inputTotalTokens = 0 - outputTotalTokens = 0 - enc = tiktoken.encoding_for_model("gpt-4") - - # Input - if isinstance(history, list): - for line in history: - inputTotalTokens += len(enc.encode(line)) - else: - inputTotalTokens += len(enc.encode(history)) - inputTotalTokens += len(enc.encode(system)) - inputTotalTokens += len(enc.encode(characters)) - inputTotalTokens += len(enc.encode(user)) - - # Output - outputTotalTokens += round(len(enc.encode(user)) * 3) - - return [inputTotalTokens, outputTotalTokens] - - -@retry(exceptions=Exception, tries=5, delay=5) -def translateGPT(text, history, fullPromptFlag): - global PBAR - - mismatch = False - totalTokens = [0, 0] - if isinstance(text, list): - tList = batchList(text, BATCHSIZE) - else: - tList = [text] - - for index, tItem in enumerate(tList): - # Before sending to translation, if we have a list of items, add the formatting - if isinstance(tItem, list): - payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) - payload = re.sub(r"(<)(\/Line\d+>)", r"\1>Placeholder Text<\3", payload) - varResponse = subVars(payload) - subbedT = varResponse[0] - else: - varResponse = subVars(tItem) - subbedT = varResponse[0] - - # Things to Check before starting translation - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): - if PBAR is not None: - PBAR.update(len(tItem)) - continue - - # Create Message - characters, system, user = createContext(fullPromptFlag, subbedT) - - # Calculate Estimate - if ESTIMATE: - estimate = countTokens(characters, system, user, history) - totalTokens[0] += estimate[0] - totalTokens[1] += estimate[1] - continue - - # Translating - response = translateText(characters, system, user, history, 0.02) - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Formatting - translatedText = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedText, True) - tList[index] = extractedTranslations - if len(tItem) != len(extractedTranslations): - # Mismatch. Try Again - response = translateText(characters, system, user, history, 0.2) - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Formatting - translatedText = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedText, True) - tList[index] = extractedTranslations - if len(tItem) != len(extractedTranslations): - mismatch = True # Just here for breakpoint - - # Create History - with LOCK: - if PBAR is not None: - PBAR.update(len(tItem)) - if not mismatch: - history = extractedTranslations[-10:] # Update history if we have a list - else: - history = text[-10:] - else: - # Ensure we're passing a single string to extractTranslation - extractedTranslations = extractTranslation(translatedText, False) - tList[index] = extractedTranslations - - # Combine if multilist - if isinstance(tList[0], list): - tList = [t for sublist in tList for t in sublist] - - # Return - if format == "json": - return [tList, totalTokens] - else: - return [tList[0], totalTokens] +# Libraries +import os +import re +import textwrap +import threading +import time +import traceback +import tiktoken +import openai +from pathlib import Path +from colorama import Fore +from dotenv import load_dotenv +from retry import retry +from tqdm import tqdm + +# Open AI +load_dotenv() +if os.getenv("api").replace(" ", "") != "": + openai.base_url = os.getenv("api") +openai.organization = os.getenv("org") +openai.api_key = os.getenv("key") + +# Globals +MODEL = os.getenv("model") +TIMEOUT = int(os.getenv("timeout")) +LANGUAGE = os.getenv("language").capitalize() +PROMPT = Path("prompt.txt").read_text(encoding="utf-8") +VOCAB = Path("vocab.txt").read_text(encoding="utf-8") +THREADS = int(os.getenv("threads")) +LOCK = threading.Lock() +WIDTH = int(os.getenv("width")) +LISTWIDTH = int(os.getenv("listWidth")) +NOTEWIDTH = 70 +MAXHISTORY = 10 +ESTIMATE = "" +TOKENS = [0, 0] +NAMESLIST = [] +NAMES = False # Output a list of all the character names found +BRFLAG = False # If the game uses
instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False +PBAR = None + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 40 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleEushully(filename, estimate): + global ESTIMATE + ESTIMATE = estimate + + if ESTIMATE: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + else: + try: + with open("translated/" + filename, "w", encoding="utf-8", errors="ignore") as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + outFile.writelines(translatedData[0]) + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + except Exception: + traceback.print_exc() + return "Fail" + + return getResultString(["", TOKENS, None], end - start, "TOTAL") + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="utf-8") as readFile: + translatedData = parseRegex(readFile, filename) + + # Delete lines marked for deletion + finalData = [] + for line in translatedData[0]: + if line != "\\d\n": + finalData.append(line) + translatedData[0] = finalData + + return translatedData + + +def parseRegex(readFile, filename): + totalTokens = [0, 0] + + # Read File into data + data = readFile.readlines() + + # Create Progress Bar + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + + try: + result = translateEushully(data, pbar, filename, []) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateEushully(data, pbar, filename, translatedList): + stringList = [] + currentGroup = [] + tokens = [0, 0] + speaker = "" + voice = False + global LOCK, ESTIMATE, PBAR + i = 0 + + while i < len(data): + voice = False + # Speaker + if "mov (global-int 46e2)" in data[i]: + # Get Speaker + speaker = re.search(r"mov \(global-int 46e2\)\s(.+)", data[i]).group(1) + response = getSpeaker(speaker) + speaker = response[0] + tokens[0] += response[1][0] + tokens[1] += response[1][1] + i += 1 + + # Show Text + if any(x in data[i] for x in ["show-text"]): + # Lines + regex = r'(.*?)"(.*)"' + match = re.search(regex, data[i]) + # Grab Strings + if match != None and match.group(2) != "": + originalString = match.group(2) + jaString = match.group(2) + currentGroup = [jaString] + while "end-text-line" in data[i + 1] and any(x in data[i + 2] for x in ["show-text"]): + match = re.search(regex, data[i + 2]) + if match != None: + currentGroup.append(match.group(2)) + if translatedList == []: + del data[i] + del data[i] + jaString = " ".join(currentGroup) + + # Pass 1 + if translatedList == []: + # Add String + if speaker: + stringList.append(f"[{speaker}]: {jaString.strip()}") + else: + stringList.append(jaString.strip()) + + # Pass 2 + else: + # Get Text + if translatedList: + # Grab and Pop + translatedText = translatedList[0] + translatedList.pop(0) + + # Set to None if empty list + if len(translatedList) <= 0: + translatedList = None + + # Replace Quotes + translatedText = translatedText.replace('"', "'") + + # Remove speaker + if speaker != "": + translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText) + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + translatedTextList = translatedText.split("\n") + + # Set Data + if len(translatedTextList) > 1: + for j in range(len(translatedTextList)): + if any(x in data[i] for x in ["show-text", "set-string", "concat"]): + del data[i] + data.insert(i, f'{match.group(1)}"{translatedTextList[j]}"\n') + i += 1 + if "end-text-line" not in data[i]: + data.insert(i, "end-text-line 0\n") + i += 1 + else: + data[i] = f'{match.group(1)}"{translatedTextList[0]}"\n' + speaker = "" + i += 1 + + # Nothing relevant. Skip Line. + else: + i += 1 + + # Set String + elif "set-string" in data[i]: + # Lines + regex = r'(.*?)"(.*)"' + match = re.search(regex, data[i]) + # Grab Strings + if match != None and match.group(2) != "": + originalString = match.group(2) + jaString = match.group(2) + currentGroup = [jaString] + + # Remove Textwrap + jaString = jaString.replace("\\n", " ") + + # Pass 1 + if translatedList == []: + # Add String + stringList.append(jaString.strip()) + + # Pass 2 + else: + # Get Text + if translatedList: + # Grab and Pop + translatedText = translatedList[0] + translatedList.pop(0) + + # Set to None if empty list + if len(translatedList) <= 0: + translatedList = None + + # Replace Quotes + translatedText = translatedText.replace('"', "'") + + # Textwrap + translatedText = textwrap.fill(translatedText, width=LISTWIDTH) + translatedText = translatedText.replace("\n", "\\n") + + # Set Data + data[i] = data[i].replace(originalString, translatedText) + speaker = "" + i += 1 + + # Nothing relevant. Skip Line. + else: + i += 1 + else: + i += 1 + + # EOF + if len(stringList) > 0: + # Set Progress + pbar.total = len(stringList) + pbar.refresh() + + # Translate + PBAR = pbar + response = translateGPT(stringList, "", True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedList = response[0] + + # Set Strings + if len(stringList) == len(translatedList): + translateEushully(data, pbar, filename, translatedList) + + # Mismatch + else: + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + return tokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + match speaker: + case "1": + return ["Klaus", [0, 0]] + case "2": + return ["Helmina", [0, 0]] + case "3": + return ["Juliana", [0, 0]] + case "4": + return ["Reginia", [0, 0]] + case "5": + return ["Luciel", [0, 0]] + case "6": + return ["Mavislaine", [0, 0]] + case "7": + return ["Cerouge", [0, 0]] + case "8": + return ["Maize", [0, 0]] + case "9": + return ["Elvire", [0, 0]] + case "a": + return ["Beatrice", [0, 0]] + case "295": + return ["Orc", [0, 0]] + case "232": + return ["Archangel", [0, 0]] + case "238": + return ["False Juliana", [0, 0]] + case "239": + return ["False Regina", [0, 0]] + case "23a": + return ["False Luciel", [0, 0]] + case "23d": + return ["False Mavislaine", [0, 0]] + case "cb": + return ["Olga Niza Kite", [0, 0]] + case "c9": + return ["Demon Beast Lupus", [0, 0]] + case "ca": + return ["Evelinael", [0, 0]] + case "10": + return ["Eukleia", [0, 0]] + case "15": + return ["Lily", [0, 0]] + case "16": + return ["Kupuko", [0, 0]] + case "b": + return ["Ramiel", [0, 0]] + case "c": + return ["Henriette", [0, 0]] + case "d": + return ["Camilla", [0, 0]] + case "cc": + return ["Gogonaua", [0, 0]] + case "65": + return ["Demon Lord Reyvalois", [0, 0]] + case "d0": + return ["Demon Ranwald", [0, 0]] + case "205": + return ["Vanqueor", [0, 0]] + case "66": + return ["Angel Martina", [0, 0]] + case "21f": + return ["Hiten Demon", [0, 0]] + case "d2": + return ["Lena Eli", [0, 0]] + case _: + return ["Unknown", [0, 0]] + + +def subVars(jaString): + jaString = jaString.replace("\u3000", " ") + + # Nested + count = 0 + nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) + nestedList = set(nestedList) + if len(nestedList) != 0: + for icon in nestedList: + jaString = jaString.replace(icon, "[Nested_" + str(count) + "]") + count += 1 + + # Icons + count = 0 + iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) + iconList = set(iconList) + if len(iconList) != 0: + for icon in iconList: + jaString = jaString.replace(icon, "[Ascii_" + str(count) + "]") + count += 1 + + # Colors + count = 0 + colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) + colorList = set(colorList) + if len(colorList) != 0: + for color in colorList: + jaString = jaString.replace(color, "[Color_" + str(count) + "]") + count += 1 + + # Names + count = 0 + nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) + nameList = set(nameList) + if len(nameList) != 0: + for name in nameList: + jaString = jaString.replace(name, "[Noun_" + str(count) + "]") + count += 1 + + # Variables + count = 0 + varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) + varList = set(varList) + if len(varList) != 0: + for var in varList: + jaString = jaString.replace(var, "[Var_" + str(count) + "]") + count += 1 + + # Formatting + count = 0 + formatList = re.findall(r"[\\]+[\w]+\[[a-zA-Z0-9\\\[\]\_,\s-]+\]", jaString) + formatList = set(formatList) + if len(formatList) != 0: + for var in formatList: + jaString = jaString.replace(var, "[FCode_" + str(count) + "]") + count += 1 + + # Put all lists in list and return + allList = [nestedList, iconList, colorList, nameList, varList, formatList] + return [jaString, allList] + + +def resubVars(translatedText, allList): + # Fix Spacing and ChatGPT Nonsense + matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) + if len(matchList) > 0: + for match in matchList: + text = match.strip() + translatedText = translatedText.replace(match, text) + + # Nested + count = 0 + if len(allList[0]) != 0: + for var in allList[0]: + translatedText = translatedText.replace("[Nested_" + str(count) + "]", var) + count += 1 + + # Icons + count = 0 + if len(allList[1]) != 0: + for var in allList[1]: + translatedText = translatedText.replace("[Ascii_" + str(count) + "]", var) + count += 1 + + # Colors + count = 0 + if len(allList[2]) != 0: + for var in allList[2]: + translatedText = translatedText.replace("[Color_" + str(count) + "]", var) + count += 1 + + # Names + count = 0 + if len(allList[3]) != 0: + for var in allList[3]: + translatedText = translatedText.replace("[Noun_" + str(count) + "]", var) + count += 1 + + # Vars + count = 0 + if len(allList[4]) != 0: + for var in allList[4]: + translatedText = translatedText.replace("[Var_" + str(count) + "]", var) + count += 1 + + # Formatting + count = 0 + if len(allList[5]) != 0: + for var in allList[5]: + translatedText = translatedText.replace("[FCode_" + str(count) + "]", var) + count += 1 + + return translatedText + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT): + characters = "Game Characters:\n\ +グレイス (Grace) - Female\n\ +" + + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + user = f"{subbedT}" + return characters, system, user + + +def translateText(characters, system, user, history, penalty): + # Prompt + msg = [{"role": "system", "content": system + characters}] + + # Characters + msg.append({"role": "system", "content": characters}) + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=MODEL, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "< ": "<", + "": ">", + "「": '"', + "」": '"', + "Placeholder Text": "", + "- chan": "-chan", + "- kun": "-kun", + "- san": "-san", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + translatedText = resubVars(translatedText, varResponse[1]) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + pattern = r"`?<[Ll]ine\d+>([\\]*.*?[\\]*?)<\/?[Ll]ine\d+>`?" + # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. + if is_list: + matchList = re.findall(pattern, translatedTextList) + return matchList + else: + matchList = re.findall(pattern, translatedTextList) + return matchList[0][0] if matchList else translatedTextList + + +def countTokens(characters, system, user, history): + inputTotalTokens = 0 + outputTotalTokens = 0 + enc = tiktoken.encoding_for_model("gpt-4") + + # Input + if isinstance(history, list): + for line in history: + inputTotalTokens += len(enc.encode(line)) + else: + inputTotalTokens += len(enc.encode(history)) + inputTotalTokens += len(enc.encode(system)) + inputTotalTokens += len(enc.encode(characters)) + inputTotalTokens += len(enc.encode(user)) + + # Output + outputTotalTokens += round(len(enc.encode(user)) * 3) + + return [inputTotalTokens, outputTotalTokens] + + +@retry(exceptions=Exception, tries=5, delay=5) +def translateGPT(text, history, fullPromptFlag): + global PBAR + + mismatch = False + totalTokens = [0, 0] + if isinstance(text, list): + tList = batchList(text, BATCHSIZE) + else: + tList = [text] + + for index, tItem in enumerate(tList): + # Before sending to translation, if we have a list of items, add the formatting + if isinstance(tItem, list): + payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) + payload = re.sub(r"(<)(\/Line\d+>)", r"\1>Placeholder Text<\3", payload) + varResponse = subVars(payload) + subbedT = varResponse[0] + else: + varResponse = subVars(tItem) + subbedT = varResponse[0] + + # Things to Check before starting translation + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): + if PBAR is not None: + PBAR.update(len(tItem)) + continue + + # Create Message + characters, system, user = createContext(fullPromptFlag, subbedT) + + # Calculate Estimate + if ESTIMATE: + estimate = countTokens(characters, system, user, history) + totalTokens[0] += estimate[0] + totalTokens[1] += estimate[1] + continue + + # Translating + response = translateText(characters, system, user, history, 0.02) + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Formatting + translatedText = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedText, True) + tList[index] = extractedTranslations + if len(tItem) != len(extractedTranslations): + # Mismatch. Try Again + response = translateText(characters, system, user, history, 0.2) + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Formatting + translatedText = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedText, True) + tList[index] = extractedTranslations + if len(tItem) != len(extractedTranslations): + mismatch = True # Just here for breakpoint + + # Create History + with LOCK: + if PBAR is not None: + PBAR.update(len(tItem)) + if not mismatch: + history = extractedTranslations[-10:] # Update history if we have a list + else: + history = text[-10:] + else: + # Ensure we're passing a single string to extractTranslation + extractedTranslations = extractTranslation(translatedText, False) + tList[index] = extractedTranslations + + # Combine if multilist + if isinstance(tList[0], list): + tList = [t for sublist in tList for t in sublist] + + # Return + if format == "json": + return [tList, totalTokens] + else: + return [tList[0], totalTokens] diff --git a/modules/irissoft.py b/modules/irissoft.py index 282c9ab..e82e858 100644 --- a/modules/irissoft.py +++ b/modules/irissoft.py @@ -1,751 +1,752 @@ -# Libraries -import os -import re -import textwrap -import threading -import time -import traceback -import tiktoken -import openai -from pathlib import Path -from colorama import Fore -from dotenv import load_dotenv -from retry import retry -from tqdm import tqdm - -# Open AI -load_dotenv() -if os.getenv("api").replace(" ", "") != "": - openai.base_url = os.getenv("api") -openai.organization = os.getenv("org") -openai.api_key = os.getenv("key") - -# Globals -MODEL = os.getenv("model") -TIMEOUT = int(os.getenv("timeout")) -LANGUAGE = os.getenv("language").capitalize() -PROMPT = Path("prompt.txt").read_text(encoding="utf-8") -VOCAB = Path("vocab.txt").read_text(encoding="utf-8") -THREADS = int(os.getenv("threads")) -LOCK = threading.Lock() -WIDTH = int(os.getenv("width")) -LISTWIDTH = int(os.getenv("listWidth")) -NOTEWIDTH = 70 -MAXHISTORY = 10 -ESTIMATE = "" -TOKENS = [0, 0] -NAMESLIST = [] -NAMES = False # Output a list of all the character names found -BRFLAG = False # If the game uses
instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 40 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleIris(filename, estimate): - global ESTIMATE - ESTIMATE = estimate - - if ESTIMATE: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - else: - try: - with open("translated/" + filename, "w", encoding="cp932", errors="ignore") as outFile: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - outFile.writelines(translatedData[0]) - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - except Exception: - traceback.print_exc() - return "Fail" - - return getResultString(["", TOKENS, None], end - start, "TOTAL") - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="shift_jis") as readFile: - translatedData = parseIris(readFile, filename) - - # Delete lines marked for deletion - finalData = [] - for line in translatedData[0]: - if line != "\\d\n": - finalData.append(line) - translatedData[0] = finalData - - return translatedData - - -def parseIris(readFile, filename): - totalTokens = [0, 0] - - # Read File into data - data = readFile.readlines() - - # Create Progress Bar - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - - try: - result = translateIris(data, pbar, filename, []) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateIris(data, pbar, filename, translatedList): - stringList = [] - currentGroup = [] - tokens = [0, 0] - speaker = "" - voice = False - global LOCK, ESTIMATE - i = 0 - - while i < len(data): - voice = False - speaker = "" - if "#MSGVOICE" in data[i]: - i += 1 - voice = True - voiceVar = data[i] - if "#MSG," in data[i] or "#MSG\n" in data[i] or voice == True: - i += 1 - # Speaker - if re.search(r'^ ?([^#\/."、。*!!()\(\)\[\] \n]+)\n', data[i]) and len(data[i]) < 30: - match = re.search(r"(.*)", data[i]) - if match != None: - speaker = match.group(1) - if speaker[0] == "\u3000": - speaker = speaker[1:] - response = getSpeaker(speaker, pbar, filename) - speaker = response[0] - tokens[0] += response[1][0] - tokens[1] += response[1][1] - if translatedList != []: - speaker = speaker.replace(" ", "\u3000") - data[i] = f"\u3000{speaker}\n" - else: - speaker = "" - i += 1 - - # Lines - match = re.search(r"(.*)", data[i]) - if match != None and match.group(1) != "": - # Pass 1 - if translatedList == []: - # Grab Consecutive Strings - jaString = data[i] - if data[i] != "\n": - if data[i][0] == "\u3000": - jaString = data[i][1:] - currentGroup.append(jaString) - i += 1 - while data[i] != "\n": - jaString = data[i] - if data[i] != "\n": - jaString = data[i][1:] - currentGroup.append(jaString) - i += 1 - - # Join up 401 groups for better translation. - if len(currentGroup) > 0: - jaString = "".join(currentGroup) - currentGroup = [] - - # Remove any textwrap - jaString = jaString.replace("\n", " ") - - # Temporarily convert spaces (For Textwrap Later) - jaString = jaString.replace("\u3000", " ") - - # Add Speaker (If there is one) - if speaker != "": - jaString = f"{speaker}: {jaString}" - - # Add String - stringList.append(jaString.strip()) - - # Pass 2 - else: - # Insert Strings - while data[i] != "\n": - data.pop(i) - - # Get Text - if translatedList: - translatedText = translatedList[0] - translatedList.pop(0) - if len(translatedList) <= 0: - translatedList = None - - # Remove added speaker - translatedText = re.sub(r"^.+?:\s", "", translatedText) - - # Textwrap - translatedText = textwrap.fill(translatedText, width=WIDTH) - translatedText = translatedText.replace("\n", "\n\u3000") - - # Replace Whitespace and Commas - translatedText = translatedText.replace(", ", "、") - translatedText = translatedText.replace(",\u3000", "、") - translatedText = translatedText.replace(",", "、") - translatedText = translatedText.replace(" ", "\u3000") - - # Set Data - # Game crashes on more than 3 lines. Will need to create a new MSG for long translations - if translatedText.count("\n") > 2: - # Split List - translatedTextList = splitNewlines(translatedText) - - # MSG Voice - count = 0 - for text in translatedTextList: - if count != 0: - if voice == True: - # MSG for each item in the list - data.insert(i, "#MSGVOICE,\n") - i += 1 - data.insert(i, f"{voiceVar}") - i += 1 - else: - data.insert(i, "#MSG,\n") - i += 1 - if speaker: - data[i] = f"\u3000{speaker}\n" - i += 1 - if text[0] == "\u3000": - data.insert(i, f"{text}\n") - else: - data.insert(i, f"\u3000{text}\n") - i += 1 - count += 1 - if data[i] != "\n": - data.insert(i, "\n") - data[i] = f"\n{data[i]}" - else: - data.insert(i, f"\u3000{translatedText}\n") - i += 1 - if data[i] != "\n": - data[i] = f"\n{data[i]}" - - elif "#SELECT" in data[i] and translatedList == []: - Iris = r"(.+?) +\d$" - i += 1 - match = re.search(Iris, data[i]) - if match: - choiceList = [] - choiceList.append(match.group(1)) - i += 1 - match = re.search(Iris, data[i]) - while match: - choiceList.append(match.group(1)) - i += 1 - match = re.search(Iris, data[i]) - - # Translate - question = stringList[len(stringList) - 1] - response = translateGPT( - choiceList, - f"Previous text for context: {question}\n\nThis will be a dialogue option", - True, - pbar, - filename, - ) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - choiceListTL = response[0] - - # Set Data - i = i - len(choiceListTL) - for j in range(len(choiceListTL)): - # Replace Whitespace and Commas - choiceListTL[j] = choiceListTL[j].replace(", ", "、") - choiceListTL[j] = choiceListTL[j].replace(",\u3000", "、") - choiceListTL[j] = choiceListTL[j].replace(",", "、") - choiceListTL[j] = choiceListTL[j].replace(" ", "\u3000") - data[i] = data[i].replace(choiceList[j], choiceListTL[j]) - i += 1 - - # Nothing relevant. Skip Line. - else: - i += 1 - else: - i += 1 - - # EOF - if len(stringList) > 0: - # Set Progress - pbar.total = len(stringList) - pbar.refresh() - - # Translate - response = translateGPT(stringList, "", True, pbar, filename) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - translatedList = response[0] - - # Set Strings - if len(stringList) == len(translatedList): - translateIris(data, pbar, filename, translatedList) - - # Mismatch - else: - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - return tokens - - -def splitNewlines(text): - parts = [] - newline_count = 0 # Counts the number of newline characters encountered - start_index = 0 # Start index of the current string part - - for i, char in enumerate(text): - if char == "\n": - newline_count += 1 - if newline_count == 3: - # Append the string part from start_index to current index (inclusive) - parts.append(text[start_index : i + 1]) - # Reset newline count and update start_index for the next string part - newline_count = 0 - start_index = i + 1 - - # Edge case: if the text does not end with a newline, we still need to append the last part - if start_index < len(text): - parts.append(text[start_index:]) - - return parts - - -# Save some money and enter the character before translation -def getSpeaker(speaker, pbar, filename): - match speaker: - case "ファイン": - return ["Fine", [0, 0]] - case "": - return ["", [0, 0]] - case _: - # Store Speaker - if speaker not in str(NAMESLIST): - response = translateGPT( - speaker, - "Reply with only the " + LANGUAGE + " translation of the NPC name.", - False, - pbar, - filename, - ) - response[0] = response[0].replace("'S", "'s") - speakerList = [speaker, response[0]] - NAMESLIST.append(speakerList) - return response - - # Find Speaker - else: - for i in range(len(NAMESLIST)): - if speaker == NAMESLIST[i][0]: - return [NAMESLIST[i][1], [0, 0]] - - return [speaker, [0, 0]] - - -def subVars(jaString): - jaString = jaString.replace("\u3000", " ") - - # Nested - count = 0 - nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) - nestedList = set(nestedList) - if len(nestedList) != 0: - for icon in nestedList: - jaString = jaString.replace(icon, "[Nested_" + str(count) + "]") - count += 1 - - # Icons - count = 0 - iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) - iconList = set(iconList) - if len(iconList) != 0: - for icon in iconList: - jaString = jaString.replace(icon, "[Ascii_" + str(count) + "]") - count += 1 - - # Colors - count = 0 - colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) - colorList = set(colorList) - if len(colorList) != 0: - for color in colorList: - jaString = jaString.replace(color, "[Color_" + str(count) + "]") - count += 1 - - # Names - count = 0 - nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) - nameList = set(nameList) - if len(nameList) != 0: - for name in nameList: - jaString = jaString.replace(name, "[Noun_" + str(count) + "]") - count += 1 - - # Variables - count = 0 - varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) - varList = set(varList) - if len(varList) != 0: - for var in varList: - jaString = jaString.replace(var, "[Var_" + str(count) + "]") - count += 1 - - # Formatting - count = 0 - formatList = re.findall(r"[\\]+[\w]+\[[a-zA-Z0-9\\\[\]\_,\s-]+\]", jaString) - formatList = set(formatList) - if len(formatList) != 0: - for var in formatList: - jaString = jaString.replace(var, "[FCode_" + str(count) + "]") - count += 1 - - # Put all lists in list and return - allList = [nestedList, iconList, colorList, nameList, varList, formatList] - return [jaString, allList] - - -def resubVars(translatedText, allList): - # Fix Spacing and ChatGPT Nonsense - matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) - if len(matchList) > 0: - for match in matchList: - text = match.strip() - translatedText = translatedText.replace(match, text) - - # Nested - count = 0 - if len(allList[0]) != 0: - for var in allList[0]: - translatedText = translatedText.replace("[Nested_" + str(count) + "]", var) - count += 1 - - # Icons - count = 0 - if len(allList[1]) != 0: - for var in allList[1]: - translatedText = translatedText.replace("[Ascii_" + str(count) + "]", var) - count += 1 - - # Colors - count = 0 - if len(allList[2]) != 0: - for var in allList[2]: - translatedText = translatedText.replace("[Color_" + str(count) + "]", var) - count += 1 - - # Names - count = 0 - if len(allList[3]) != 0: - for var in allList[3]: - translatedText = translatedText.replace("[Noun_" + str(count) + "]", var) - count += 1 - - # Vars - count = 0 - if len(allList[4]) != 0: - for var in allList[4]: - translatedText = translatedText.replace("[Var_" + str(count) + "]", var) - count += 1 - - # Formatting - count = 0 - if len(allList[5]) != 0: - for var in allList[5]: - translatedText = translatedText.replace("[FCode_" + str(count) + "]", var) - count += 1 - - return translatedText - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT): - characters = "Game Characters:\n\ -フィリア (Philia) - Female\n\ -アルネット (Annett) - Female\n\ -ラピュセナ (Rapusena) - Female\n\ -リッカ (Rikka) - Female\n\ -アンデリビア (Andelivia) - Female\n\ -リリアブルム (Liliabloom) - Female\n\ -カルナ (Karna) - Female\n\ -ラフィング=スピア (Laughing Spear) - Female\n\ -ノーラ (Nora) - Female\n\ -" - - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - user = f"{subbedT}" - return characters, system, user - - -def translateText(characters, system, user, history): - # Prompt - msg = [{"role": "system", "content": system + characters}] - - # Characters - msg.append({"role": "system", "content": characters}) - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0.1, - frequency_penalty=0.1, - model=MODEL, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - translatedText = resubVars(translatedText, varResponse[1]) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - pattern = r"`?<[Ll]ine\d+>([\\]*.*?[\\]*?)<\/?[Ll]ine\d+>`?" - # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. - if is_list: - matchList = re.findall(pattern, translatedTextList) - return matchList - else: - matchList = re.findall(pattern, translatedTextList) - return matchList[0][0] if matchList else translatedTextList - - -def countTokens(characters, system, user, history): - inputTotalTokens = 0 - outputTotalTokens = 0 - enc = tiktoken.encoding_for_model("gpt-4") - - # Input - if isinstance(history, list): - for line in history: - inputTotalTokens += len(enc.encode(line)) - else: - inputTotalTokens += len(enc.encode(history)) - inputTotalTokens += len(enc.encode(system)) - inputTotalTokens += len(enc.encode(characters)) - inputTotalTokens += len(enc.encode(user)) - - # Output - outputTotalTokens += round(len(enc.encode(user)) * 2) - - return [inputTotalTokens, outputTotalTokens] - - -@retry(exceptions=Exception, tries=5, delay=5) -def translateGPT(text, history, fullPromptFlag, pbar, filename): - mismatch = False - totalTokens = [0, 0] - if isinstance(text, list): - tList = batchList(text, BATCHSIZE) - else: - tList = [text] - - for index, tItem in enumerate(tList): - # Before sending to translation, if we have a list of items, add the formatting - if isinstance(tItem, list): - payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) - payload = re.sub(r"(<)(\/Line\d+>)", r"\1>Placeholder Text<\3", payload) - varResponse = subVars(payload) - subbedT = varResponse[0] - else: - varResponse = subVars(tItem) - subbedT = varResponse[0] - - # Things to Check before starting translation - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): - continue - - # Create Message - characters, system, user = createContext(fullPromptFlag, subbedT) - - # Calculate Estimate - if ESTIMATE: - estimate = countTokens(characters, system, user, history) - totalTokens[0] += estimate[0] - totalTokens[1] += estimate[1] - continue - - # Translating - response = translateText(characters, system, user, history) - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Formatting - translatedText = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedText, True) - if len(tItem) != len(extractedTranslations): - # Mismatch. Try Again - response = translateText(characters, system, user, history) - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Formatting - translatedText = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedText, True) - if len(tItem) == len(extractedTranslations): - tList[index] = extractedTranslations - else: - MISMATCH.append(filename) - else: - tList[index] = extractedTranslations - - # Create History - history = tList[index] # Update history if we have a list - pbar.update(len(tList[index])) - - else: - # Ensure we're passing a single string to extractTranslation - extractedTranslations = extractTranslation(translatedText, False) - tList[index] = extractedTranslations - - # Combine if multilist - if isinstance(tList[0], list): - tList = [t for sublist in tList for t in sublist] - - # Return - if format == "json": - return [tList, totalTokens] - else: - return [tList[0], totalTokens] +# Libraries +import os +import re +import textwrap +import threading +import time +import traceback +import tiktoken +import openai +from pathlib import Path +from colorama import Fore +from dotenv import load_dotenv +from retry import retry +from tqdm import tqdm + +# Open AI +load_dotenv() +if os.getenv("api").replace(" ", "") != "": + openai.base_url = os.getenv("api") +openai.organization = os.getenv("org") +openai.api_key = os.getenv("key") + +# Globals +MODEL = os.getenv("model") +TIMEOUT = int(os.getenv("timeout")) +LANGUAGE = os.getenv("language").capitalize() +PROMPT = Path("prompt.txt").read_text(encoding="utf-8") +VOCAB = Path("vocab.txt").read_text(encoding="utf-8") +THREADS = int(os.getenv("threads")) +LOCK = threading.Lock() +WIDTH = int(os.getenv("width")) +LISTWIDTH = int(os.getenv("listWidth")) +NOTEWIDTH = 70 +MAXHISTORY = 10 +ESTIMATE = "" +TOKENS = [0, 0] +NAMESLIST = [] +NAMES = False # Output a list of all the character names found +BRFLAG = False # If the game uses
instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 40 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleIris(filename, estimate): + global ESTIMATE + ESTIMATE = estimate + + if ESTIMATE: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + else: + try: + with open("translated/" + filename, "w", encoding="cp932", errors="ignore") as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + outFile.writelines(translatedData[0]) + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + except Exception: + traceback.print_exc() + return "Fail" + + return getResultString(["", TOKENS, None], end - start, "TOTAL") + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="shift_jis") as readFile: + translatedData = parseIris(readFile, filename) + + # Delete lines marked for deletion + finalData = [] + for line in translatedData[0]: + if line != "\\d\n": + finalData.append(line) + translatedData[0] = finalData + + return translatedData + + +def parseIris(readFile, filename): + totalTokens = [0, 0] + + # Read File into data + data = readFile.readlines() + + # Create Progress Bar + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + + try: + result = translateIris(data, pbar, filename, []) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateIris(data, pbar, filename, translatedList): + stringList = [] + currentGroup = [] + tokens = [0, 0] + speaker = "" + voice = False + global LOCK, ESTIMATE + i = 0 + + while i < len(data): + voice = False + speaker = "" + if "#MSGVOICE" in data[i]: + i += 1 + voice = True + voiceVar = data[i] + if "#MSG," in data[i] or "#MSG\n" in data[i] or voice == True: + i += 1 + # Speaker + if re.search(r'^ ?([^#\/."、。*!!()\(\)\[\] \n]+)\n', data[i]) and len(data[i]) < 30: + match = re.search(r"(.*)", data[i]) + if match != None: + speaker = match.group(1) + if speaker[0] == "\u3000": + speaker = speaker[1:] + response = getSpeaker(speaker, pbar, filename) + speaker = response[0] + tokens[0] += response[1][0] + tokens[1] += response[1][1] + if translatedList != []: + speaker = speaker.replace(" ", "\u3000") + data[i] = f"\u3000{speaker}\n" + else: + speaker = "" + i += 1 + + # Lines + match = re.search(r"(.*)", data[i]) + if match != None and match.group(1) != "": + # Pass 1 + if translatedList == []: + # Grab Consecutive Strings + jaString = data[i] + if data[i] != "\n": + if data[i][0] == "\u3000": + jaString = data[i][1:] + currentGroup.append(jaString) + i += 1 + while data[i] != "\n": + jaString = data[i] + if data[i] != "\n": + jaString = data[i][1:] + currentGroup.append(jaString) + i += 1 + + # Join up 401 groups for better translation. + if len(currentGroup) > 0: + jaString = "".join(currentGroup) + currentGroup = [] + + # Remove any textwrap + jaString = jaString.replace("\n", " ") + + # Temporarily convert spaces (For Textwrap Later) + jaString = jaString.replace("\u3000", " ") + + # Add Speaker (If there is one) + if speaker != "": + jaString = f"{speaker}: {jaString}" + + # Add String + stringList.append(jaString.strip()) + + # Pass 2 + else: + # Insert Strings + while data[i] != "\n": + data.pop(i) + + # Get Text + if translatedList: + translatedText = translatedList[0] + translatedList.pop(0) + if len(translatedList) <= 0: + translatedList = None + + # Remove added speaker + translatedText = re.sub(r"^.+?:\s", "", translatedText) + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + translatedText = translatedText.replace("\n", "\n\u3000") + + # Replace Whitespace and Commas + translatedText = translatedText.replace(", ", "、") + translatedText = translatedText.replace(",\u3000", "、") + translatedText = translatedText.replace(",", "、") + translatedText = translatedText.replace(" ", "\u3000") + + # Set Data + # Game crashes on more than 3 lines. Will need to create a new MSG for long translations + if translatedText.count("\n") > 2: + # Split List + translatedTextList = splitNewlines(translatedText) + + # MSG Voice + count = 0 + for text in translatedTextList: + if count != 0: + if voice == True: + # MSG for each item in the list + data.insert(i, "#MSGVOICE,\n") + i += 1 + data.insert(i, f"{voiceVar}") + i += 1 + else: + data.insert(i, "#MSG,\n") + i += 1 + if speaker: + data[i] = f"\u3000{speaker}\n" + i += 1 + if text[0] == "\u3000": + data.insert(i, f"{text}\n") + else: + data.insert(i, f"\u3000{text}\n") + i += 1 + count += 1 + if data[i] != "\n": + data.insert(i, "\n") + data[i] = f"\n{data[i]}" + else: + data.insert(i, f"\u3000{translatedText}\n") + i += 1 + if data[i] != "\n": + data[i] = f"\n{data[i]}" + + elif "#SELECT" in data[i] and translatedList == []: + Iris = r"(.+?) +\d$" + i += 1 + match = re.search(Iris, data[i]) + if match: + choiceList = [] + choiceList.append(match.group(1)) + i += 1 + match = re.search(Iris, data[i]) + while match: + choiceList.append(match.group(1)) + i += 1 + match = re.search(Iris, data[i]) + + # Translate + question = stringList[len(stringList) - 1] + response = translateGPT( + choiceList, + f"Previous text for context: {question}\n\nThis will be a dialogue option", + True, + pbar, + filename, + ) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + choiceListTL = response[0] + + # Set Data + i = i - len(choiceListTL) + for j in range(len(choiceListTL)): + # Replace Whitespace and Commas + choiceListTL[j] = choiceListTL[j].replace(", ", "、") + choiceListTL[j] = choiceListTL[j].replace(",\u3000", "、") + choiceListTL[j] = choiceListTL[j].replace(",", "、") + choiceListTL[j] = choiceListTL[j].replace(" ", "\u3000") + data[i] = data[i].replace(choiceList[j], choiceListTL[j]) + i += 1 + + # Nothing relevant. Skip Line. + else: + i += 1 + else: + i += 1 + + # EOF + if len(stringList) > 0: + # Set Progress + pbar.total = len(stringList) + pbar.refresh() + + # Translate + response = translateGPT(stringList, "", True, pbar, filename) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedList = response[0] + + # Set Strings + if len(stringList) == len(translatedList): + translateIris(data, pbar, filename, translatedList) + + # Mismatch + else: + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + return tokens + + +def splitNewlines(text): + parts = [] + newline_count = 0 # Counts the number of newline characters encountered + start_index = 0 # Start index of the current string part + + for i, char in enumerate(text): + if char == "\n": + newline_count += 1 + if newline_count == 3: + # Append the string part from start_index to current index (inclusive) + parts.append(text[start_index : i + 1]) + # Reset newline count and update start_index for the next string part + newline_count = 0 + start_index = i + 1 + + # Edge case: if the text does not end with a newline, we still need to append the last part + if start_index < len(text): + parts.append(text[start_index:]) + + return parts + + +# Save some money and enter the character before translation +def getSpeaker(speaker, pbar, filename): + match speaker: + case "ファイン": + return ["Fine", [0, 0]] + case "": + return ["", [0, 0]] + case _: + # Store Speaker + if speaker not in str(NAMESLIST): + response = translateGPT( + speaker, + "Reply with only the " + LANGUAGE + " translation of the NPC name.", + False, + pbar, + filename, + ) + response[0] = response[0].replace("'S", "'s") + speakerList = [speaker, response[0]] + NAMESLIST.append(speakerList) + return response + + # Find Speaker + else: + for i in range(len(NAMESLIST)): + if speaker == NAMESLIST[i][0]: + return [NAMESLIST[i][1], [0, 0]] + + return [speaker, [0, 0]] + + +def subVars(jaString): + jaString = jaString.replace("\u3000", " ") + + # Nested + count = 0 + nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) + nestedList = set(nestedList) + if len(nestedList) != 0: + for icon in nestedList: + jaString = jaString.replace(icon, "[Nested_" + str(count) + "]") + count += 1 + + # Icons + count = 0 + iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) + iconList = set(iconList) + if len(iconList) != 0: + for icon in iconList: + jaString = jaString.replace(icon, "[Ascii_" + str(count) + "]") + count += 1 + + # Colors + count = 0 + colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) + colorList = set(colorList) + if len(colorList) != 0: + for color in colorList: + jaString = jaString.replace(color, "[Color_" + str(count) + "]") + count += 1 + + # Names + count = 0 + nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) + nameList = set(nameList) + if len(nameList) != 0: + for name in nameList: + jaString = jaString.replace(name, "[Noun_" + str(count) + "]") + count += 1 + + # Variables + count = 0 + varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) + varList = set(varList) + if len(varList) != 0: + for var in varList: + jaString = jaString.replace(var, "[Var_" + str(count) + "]") + count += 1 + + # Formatting + count = 0 + formatList = re.findall(r"[\\]+[\w]+\[[a-zA-Z0-9\\\[\]\_,\s-]+\]", jaString) + formatList = set(formatList) + if len(formatList) != 0: + for var in formatList: + jaString = jaString.replace(var, "[FCode_" + str(count) + "]") + count += 1 + + # Put all lists in list and return + allList = [nestedList, iconList, colorList, nameList, varList, formatList] + return [jaString, allList] + + +def resubVars(translatedText, allList): + # Fix Spacing and ChatGPT Nonsense + matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) + if len(matchList) > 0: + for match in matchList: + text = match.strip() + translatedText = translatedText.replace(match, text) + + # Nested + count = 0 + if len(allList[0]) != 0: + for var in allList[0]: + translatedText = translatedText.replace("[Nested_" + str(count) + "]", var) + count += 1 + + # Icons + count = 0 + if len(allList[1]) != 0: + for var in allList[1]: + translatedText = translatedText.replace("[Ascii_" + str(count) + "]", var) + count += 1 + + # Colors + count = 0 + if len(allList[2]) != 0: + for var in allList[2]: + translatedText = translatedText.replace("[Color_" + str(count) + "]", var) + count += 1 + + # Names + count = 0 + if len(allList[3]) != 0: + for var in allList[3]: + translatedText = translatedText.replace("[Noun_" + str(count) + "]", var) + count += 1 + + # Vars + count = 0 + if len(allList[4]) != 0: + for var in allList[4]: + translatedText = translatedText.replace("[Var_" + str(count) + "]", var) + count += 1 + + # Formatting + count = 0 + if len(allList[5]) != 0: + for var in allList[5]: + translatedText = translatedText.replace("[FCode_" + str(count) + "]", var) + count += 1 + + return translatedText + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT): + characters = "Game Characters:\n\ +フィリア (Philia) - Female\n\ +アルネット (Annett) - Female\n\ +ラピュセナ (Rapusena) - Female\n\ +リッカ (Rikka) - Female\n\ +アンデリビア (Andelivia) - Female\n\ +リリアブルム (Liliabloom) - Female\n\ +カルナ (Karna) - Female\n\ +ラフィング=スピア (Laughing Spear) - Female\n\ +ノーラ (Nora) - Female\n\ +" + + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + user = f"{subbedT}" + return characters, system, user + + +def translateText(characters, system, user, history): + # Prompt + msg = [{"role": "system", "content": system + characters}] + + # Characters + msg.append({"role": "system", "content": characters}) + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0.1, + frequency_penalty=0.1, + model=MODEL, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + translatedText = resubVars(translatedText, varResponse[1]) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + pattern = r"`?<[Ll]ine\d+>([\\]*.*?[\\]*?)<\/?[Ll]ine\d+>`?" + # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. + if is_list: + matchList = re.findall(pattern, translatedTextList) + return matchList + else: + matchList = re.findall(pattern, translatedTextList) + return matchList[0][0] if matchList else translatedTextList + + +def countTokens(characters, system, user, history): + inputTotalTokens = 0 + outputTotalTokens = 0 + enc = tiktoken.encoding_for_model("gpt-4") + + # Input + if isinstance(history, list): + for line in history: + inputTotalTokens += len(enc.encode(line)) + else: + inputTotalTokens += len(enc.encode(history)) + inputTotalTokens += len(enc.encode(system)) + inputTotalTokens += len(enc.encode(characters)) + inputTotalTokens += len(enc.encode(user)) + + # Output + outputTotalTokens += round(len(enc.encode(user)) * 2) + + return [inputTotalTokens, outputTotalTokens] + + +@retry(exceptions=Exception, tries=5, delay=5) +def translateGPT(text, history, fullPromptFlag, pbar, filename): + mismatch = False + totalTokens = [0, 0] + if isinstance(text, list): + tList = batchList(text, BATCHSIZE) + else: + tList = [text] + + for index, tItem in enumerate(tList): + # Before sending to translation, if we have a list of items, add the formatting + if isinstance(tItem, list): + payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) + payload = re.sub(r"(<)(\/Line\d+>)", r"\1>Placeholder Text<\3", payload) + varResponse = subVars(payload) + subbedT = varResponse[0] + else: + varResponse = subVars(tItem) + subbedT = varResponse[0] + + # Things to Check before starting translation + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): + continue + + # Create Message + characters, system, user = createContext(fullPromptFlag, subbedT) + + # Calculate Estimate + if ESTIMATE: + estimate = countTokens(characters, system, user, history) + totalTokens[0] += estimate[0] + totalTokens[1] += estimate[1] + continue + + # Translating + response = translateText(characters, system, user, history) + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Formatting + translatedText = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedText, True) + if len(tItem) != len(extractedTranslations): + # Mismatch. Try Again + response = translateText(characters, system, user, history) + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Formatting + translatedText = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedText, True) + if len(tItem) == len(extractedTranslations): + tList[index] = extractedTranslations + else: + MISMATCH.append(filename) + else: + tList[index] = extractedTranslations + + # Create History + history = tList[index] # Update history if we have a list + pbar.update(len(tList[index])) + + else: + # Ensure we're passing a single string to extractTranslation + extractedTranslations = extractTranslation(translatedText, False) + tList[index] = extractedTranslations + + # Combine if multilist + if isinstance(tList[0], list): + tList = [t for sublist in tList for t in sublist] + + # Return + if format == "json": + return [tList, totalTokens] + else: + return [tList[0], totalTokens] diff --git a/modules/javascript.py b/modules/javascript.py index ed3e609..bc7e2b3 100644 --- a/modules/javascript.py +++ b/modules/javascript.py @@ -1,526 +1,527 @@ -# Libraries -import os -import re -import textwrap -import threading -import time -import traceback -import tiktoken -import openai -from pathlib import Path -from colorama import Fore -from dotenv import load_dotenv -from retry import retry -from tqdm import tqdm - -# Open AI -load_dotenv() -if os.getenv("api").replace(" ", "") != "": - openai.base_url = os.getenv("api") -openai.organization = os.getenv("org") -openai.api_key = os.getenv("key") - -# Globals -MODEL = os.getenv("model") -TIMEOUT = int(os.getenv("timeout")) -LANGUAGE = os.getenv("language").capitalize() -PROMPT = Path("prompt.txt").read_text(encoding="utf-8") -VOCAB = Path("vocab.txt").read_text(encoding="utf-8") -THREADS = int(os.getenv("threads")) -LOCK = threading.Lock() -WIDTH = int(os.getenv("width")) -LISTWIDTH = int(os.getenv("listWidth")) -NOTEWIDTH = 70 -MAXHISTORY = 10 -ESTIMATE = "" -TOKENS = [0, 0] -NAMESLIST = [] -NAMES = False # Output a list of all the character names found -BRFLAG = False # If the game uses
instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 40 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleJavascript(filename, estimate): - global ESTIMATE - ESTIMATE = estimate - - if ESTIMATE: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - else: - try: - with open("translated/" + filename, "w", encoding="utf8", errors="ignore") as outFile: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - outFile.writelines(translatedData[0]) - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - except Exception: - traceback.print_exc() - return "Fail" - - return getResultString(["", TOKENS, None], end - start, "TOTAL") - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="utf-8") as readFile: - translatedData = parseJS(readFile, filename) - - return translatedData - - -def parseJS(readFile, filename): - totalTokens = [0, 0] - data = readFile.readlines() - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - - try: - result = translateJS(data, pbar) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateJS(data, pbar): - tokens = [0, 0] - i = 0 - - # Regex & Plugin Name - regex = r'ObjectiveContent[\\]+":[\\]+"[\\]+"(.*?)[\\]+"' - - # Find Plugin - while i < len(data): - # Run Search - stringList = re.findall(regex, data[i]) - if len(stringList) != 0: - pbar.total = len(stringList) - pbar.refresh() - modifiedStringList = stringList.copy() - - # Remove Wordwrap [Optional] - for j in range(len(modifiedStringList)): - modifiedStringList[j] = modifiedStringList[j].replace(r"\\\\\\\\n", r" ") - - # Translate - response = translateGPT(modifiedStringList, f"Reply with the {LANGUAGE} translation", True, pbar) - translatedList = response[0] - tokens[0] = response[1][0] - tokens[0] = response[1][1] - - # Validate Length & Replace Each Match - if len(translatedList) == len(modifiedStringList): - for j in range(len(translatedList)): - # Add escape for ' - translatedList[j] = re.sub(r"[^\\](')", "\\'", translatedList[j]) - - # Wordwrap [Optional] - translatedList[j] = textwrap.fill(translatedList[j], LISTWIDTH) - translatedList[j] = translatedList[j].replace("\n", r"\\\\\\\\n") - - # Set - data[i] = data[i].replace(stringList[j], translatedList[j]) - # Mismatch - else: - pbar.write("Mismatch Error") - i += 1 - - return tokens - - -def subVars(jaString): - jaString = jaString.replace("\u3000", " ") - - # Nested - count = 0 - nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) - nestedList = set(nestedList) - if len(nestedList) != 0: - for icon in nestedList: - jaString = jaString.replace(icon, "[Nested_" + str(count) + "]") - count += 1 - - # Icons - count = 0 - iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) - iconList = set(iconList) - if len(iconList) != 0: - for icon in iconList: - jaString = jaString.replace(icon, "[Ascii_" + str(count) + "]") - count += 1 - - # Colors - count = 0 - colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) - colorList = set(colorList) - if len(colorList) != 0: - for color in colorList: - jaString = jaString.replace(color, "[Color_" + str(count) + "]") - count += 1 - - # Names - count = 0 - nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) - nameList = set(nameList) - if len(nameList) != 0: - for name in nameList: - jaString = jaString.replace(name, "[Noun_" + str(count) + "]") - count += 1 - - # Variables - count = 0 - varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) - varList = set(varList) - if len(varList) != 0: - for var in varList: - jaString = jaString.replace(var, "[Var_" + str(count) + "]") - count += 1 - - # Formatting - count = 0 - formatList = re.findall(r"[\\]+[\w]+\[[a-zA-Z0-9\\\[\]\_,\s-]+\]", jaString) - formatList = set(formatList) - if len(formatList) != 0: - for var in formatList: - jaString = jaString.replace(var, "[FCode_" + str(count) + "]") - count += 1 - - # Put all lists in list and return - allList = [nestedList, iconList, colorList, nameList, varList, formatList] - return [jaString, allList] - - -def resubVars(translatedText, allList): - # Fix Spacing and ChatGPT Nonsense - matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) - if len(matchList) > 0: - for match in matchList: - text = match.strip() - translatedText = translatedText.replace(match, text) - - # Nested - count = 0 - if len(allList[0]) != 0: - for var in allList[0]: - translatedText = translatedText.replace("[Nested_" + str(count) + "]", var) - count += 1 - - # Icons - count = 0 - if len(allList[1]) != 0: - for var in allList[1]: - translatedText = translatedText.replace("[Ascii_" + str(count) + "]", var) - count += 1 - - # Colors - count = 0 - if len(allList[2]) != 0: - for var in allList[2]: - translatedText = translatedText.replace("[Color_" + str(count) + "]", var) - count += 1 - - # Names - count = 0 - if len(allList[3]) != 0: - for var in allList[3]: - translatedText = translatedText.replace("[Noun_" + str(count) + "]", var) - count += 1 - - # Vars - count = 0 - if len(allList[4]) != 0: - for var in allList[4]: - translatedText = translatedText.replace("[Var_" + str(count) + "]", var) - count += 1 - - # Formatting - count = 0 - if len(allList[5]) != 0: - for var in allList[5]: - translatedText = translatedText.replace("[FCode_" + str(count) + "]", var) - count += 1 - - return translatedText - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT): - characters = "Game Characters:\n\ -皆月 (Minazuki)\n\ -さやか (Sayaka)\n\ -皆月 さやか (Minazuki Sayaka) - Female\n\ -広瀬 (Hirose)\n\ -智恵 (Chie) - Female\n\ -広瀬 智恵 (Hirose Chie) - Female\n\ -" - - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - user = f"{subbedT}" - return characters, system, user - - -def translateText(characters, system, user, history): - # Prompt - msg = [{"role": "system", "content": system + characters}] - - # Characters - msg.append({"role": "system", "content": characters}) - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0.1, - frequency_penalty=0.1, - model=MODEL, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - translatedText = resubVars(translatedText, varResponse[1]) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - pattern = r"`?<[Ll]ine\d+>([\\]*.*?[\\]*?)<\/?[Ll]ine\d+>`?" - # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. - if is_list: - matchList = re.findall(pattern, translatedTextList) - return matchList - else: - matchList = re.findall(pattern, translatedTextList) - return matchList[0][0] if matchList else translatedTextList - - -def countTokens(characters, system, user, history): - inputTotalTokens = 0 - outputTotalTokens = 0 - enc = tiktoken.encoding_for_model("gpt-4") - - # Input - if isinstance(history, list): - for line in history: - inputTotalTokens += len(enc.encode(line)) - else: - inputTotalTokens += len(enc.encode(history)) - inputTotalTokens += len(enc.encode(system)) - inputTotalTokens += len(enc.encode(characters)) - inputTotalTokens += len(enc.encode(user)) - - # Output - outputTotalTokens += round(len(enc.encode(user)) * 3) - - return [inputTotalTokens, outputTotalTokens] - - -@retry(exceptions=Exception, tries=5, delay=5) -def translateGPT(text, history, fullPromptFlag, pbar): - mismatch = False - totalTokens = [0, 0] - if isinstance(text, list): - tList = batchList(text, BATCHSIZE) - else: - tList = [text] - - for index, tItem in enumerate(tList): - # Before sending to translation, if we have a list of items, add the formatting - if isinstance(tItem, list): - payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) - payload = re.sub(r"(<)(\/Line\d+>)", r"\1>Placeholder Text<\3", payload) - varResponse = subVars(payload) - subbedT = varResponse[0] - else: - varResponse = subVars(tItem) - subbedT = varResponse[0] - - # Things to Check before starting translation - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): - continue - - # Create Message - characters, system, user = createContext(fullPromptFlag, subbedT) - - # Calculate Estimate - if ESTIMATE: - estimate = countTokens(characters, system, user, history) - totalTokens[0] += estimate[0] - totalTokens[1] += estimate[1] - continue - - # Translating - response = translateText(characters, system, user, history) - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Formatting - translatedText = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedText, True) - tList[index] = extractedTranslations - if len(tItem) != len(extractedTranslations): - # Mismatch. Try Again - response = translateText(characters, system, user, history) - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Formatting - translatedText = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedText, True) - if len(tItem) == len(extractedTranslations): - tList[index] = extractedTranslations - else: - mismatch = True # Just here for breakpoint - - # Create History - history = tList[index] # Update history if we have a list - pbar.update(len(tList[index])) - - else: - # Ensure we're passing a single string to extractTranslation - extractedTranslations = extractTranslation(translatedText, False) - tList[index] = extractedTranslations - - # Combine if multilist - if isinstance(tList[0], list): - tList = [t for sublist in tList for t in sublist] - - # Return - if format == "json": - return [tList, totalTokens] - else: - return [tList[0], totalTokens] +# Libraries +import os +import re +import textwrap +import threading +import time +import traceback +import tiktoken +import openai +from pathlib import Path +from colorama import Fore +from dotenv import load_dotenv +from retry import retry +from tqdm import tqdm + +# Open AI +load_dotenv() +if os.getenv("api").replace(" ", "") != "": + openai.base_url = os.getenv("api") +openai.organization = os.getenv("org") +openai.api_key = os.getenv("key") + +# Globals +MODEL = os.getenv("model") +TIMEOUT = int(os.getenv("timeout")) +LANGUAGE = os.getenv("language").capitalize() +PROMPT = Path("prompt.txt").read_text(encoding="utf-8") +VOCAB = Path("vocab.txt").read_text(encoding="utf-8") +THREADS = int(os.getenv("threads")) +LOCK = threading.Lock() +WIDTH = int(os.getenv("width")) +LISTWIDTH = int(os.getenv("listWidth")) +NOTEWIDTH = 70 +MAXHISTORY = 10 +ESTIMATE = "" +TOKENS = [0, 0] +NAMESLIST = [] +NAMES = False # Output a list of all the character names found +BRFLAG = False # If the game uses
instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 40 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleJavascript(filename, estimate): + global ESTIMATE + ESTIMATE = estimate + + if ESTIMATE: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + else: + try: + with open("translated/" + filename, "w", encoding="utf8", errors="ignore") as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + outFile.writelines(translatedData[0]) + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + except Exception: + traceback.print_exc() + return "Fail" + + return getResultString(["", TOKENS, None], end - start, "TOTAL") + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="utf-8") as readFile: + translatedData = parseJS(readFile, filename) + + return translatedData + + +def parseJS(readFile, filename): + totalTokens = [0, 0] + data = readFile.readlines() + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + + try: + result = translateJS(data, pbar) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateJS(data, pbar): + tokens = [0, 0] + i = 0 + + # Regex & Plugin Name + regex = r'ObjectiveContent[\\]+":[\\]+"[\\]+"(.*?)[\\]+"' + + # Find Plugin + while i < len(data): + # Run Search + stringList = re.findall(regex, data[i]) + if len(stringList) != 0: + pbar.total = len(stringList) + pbar.refresh() + modifiedStringList = stringList.copy() + + # Remove Wordwrap [Optional] + for j in range(len(modifiedStringList)): + modifiedStringList[j] = modifiedStringList[j].replace(r"\\\\\\\\n", r" ") + + # Translate + response = translateGPT(modifiedStringList, f"Reply with the {LANGUAGE} translation", True, pbar) + translatedList = response[0] + tokens[0] = response[1][0] + tokens[0] = response[1][1] + + # Validate Length & Replace Each Match + if len(translatedList) == len(modifiedStringList): + for j in range(len(translatedList)): + # Add escape for ' + translatedList[j] = re.sub(r"[^\\](')", "\\'", translatedList[j]) + + # Wordwrap [Optional] + translatedList[j] = textwrap.fill(translatedList[j], LISTWIDTH) + translatedList[j] = translatedList[j].replace("\n", r"\\\\\\\\n") + + # Set + data[i] = data[i].replace(stringList[j], translatedList[j]) + # Mismatch + else: + pbar.write("Mismatch Error") + i += 1 + + return tokens + + +def subVars(jaString): + jaString = jaString.replace("\u3000", " ") + + # Nested + count = 0 + nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) + nestedList = set(nestedList) + if len(nestedList) != 0: + for icon in nestedList: + jaString = jaString.replace(icon, "[Nested_" + str(count) + "]") + count += 1 + + # Icons + count = 0 + iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) + iconList = set(iconList) + if len(iconList) != 0: + for icon in iconList: + jaString = jaString.replace(icon, "[Ascii_" + str(count) + "]") + count += 1 + + # Colors + count = 0 + colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) + colorList = set(colorList) + if len(colorList) != 0: + for color in colorList: + jaString = jaString.replace(color, "[Color_" + str(count) + "]") + count += 1 + + # Names + count = 0 + nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) + nameList = set(nameList) + if len(nameList) != 0: + for name in nameList: + jaString = jaString.replace(name, "[Noun_" + str(count) + "]") + count += 1 + + # Variables + count = 0 + varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) + varList = set(varList) + if len(varList) != 0: + for var in varList: + jaString = jaString.replace(var, "[Var_" + str(count) + "]") + count += 1 + + # Formatting + count = 0 + formatList = re.findall(r"[\\]+[\w]+\[[a-zA-Z0-9\\\[\]\_,\s-]+\]", jaString) + formatList = set(formatList) + if len(formatList) != 0: + for var in formatList: + jaString = jaString.replace(var, "[FCode_" + str(count) + "]") + count += 1 + + # Put all lists in list and return + allList = [nestedList, iconList, colorList, nameList, varList, formatList] + return [jaString, allList] + + +def resubVars(translatedText, allList): + # Fix Spacing and ChatGPT Nonsense + matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) + if len(matchList) > 0: + for match in matchList: + text = match.strip() + translatedText = translatedText.replace(match, text) + + # Nested + count = 0 + if len(allList[0]) != 0: + for var in allList[0]: + translatedText = translatedText.replace("[Nested_" + str(count) + "]", var) + count += 1 + + # Icons + count = 0 + if len(allList[1]) != 0: + for var in allList[1]: + translatedText = translatedText.replace("[Ascii_" + str(count) + "]", var) + count += 1 + + # Colors + count = 0 + if len(allList[2]) != 0: + for var in allList[2]: + translatedText = translatedText.replace("[Color_" + str(count) + "]", var) + count += 1 + + # Names + count = 0 + if len(allList[3]) != 0: + for var in allList[3]: + translatedText = translatedText.replace("[Noun_" + str(count) + "]", var) + count += 1 + + # Vars + count = 0 + if len(allList[4]) != 0: + for var in allList[4]: + translatedText = translatedText.replace("[Var_" + str(count) + "]", var) + count += 1 + + # Formatting + count = 0 + if len(allList[5]) != 0: + for var in allList[5]: + translatedText = translatedText.replace("[FCode_" + str(count) + "]", var) + count += 1 + + return translatedText + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT): + characters = "Game Characters:\n\ +皆月 (Minazuki)\n\ +さやか (Sayaka)\n\ +皆月 さやか (Minazuki Sayaka) - Female\n\ +広瀬 (Hirose)\n\ +智恵 (Chie) - Female\n\ +広瀬 智恵 (Hirose Chie) - Female\n\ +" + + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + user = f"{subbedT}" + return characters, system, user + + +def translateText(characters, system, user, history): + # Prompt + msg = [{"role": "system", "content": system + characters}] + + # Characters + msg.append({"role": "system", "content": characters}) + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0.1, + frequency_penalty=0.1, + model=MODEL, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + translatedText = resubVars(translatedText, varResponse[1]) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + pattern = r"`?<[Ll]ine\d+>([\\]*.*?[\\]*?)<\/?[Ll]ine\d+>`?" + # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. + if is_list: + matchList = re.findall(pattern, translatedTextList) + return matchList + else: + matchList = re.findall(pattern, translatedTextList) + return matchList[0][0] if matchList else translatedTextList + + +def countTokens(characters, system, user, history): + inputTotalTokens = 0 + outputTotalTokens = 0 + enc = tiktoken.encoding_for_model("gpt-4") + + # Input + if isinstance(history, list): + for line in history: + inputTotalTokens += len(enc.encode(line)) + else: + inputTotalTokens += len(enc.encode(history)) + inputTotalTokens += len(enc.encode(system)) + inputTotalTokens += len(enc.encode(characters)) + inputTotalTokens += len(enc.encode(user)) + + # Output + outputTotalTokens += round(len(enc.encode(user)) * 3) + + return [inputTotalTokens, outputTotalTokens] + + +@retry(exceptions=Exception, tries=5, delay=5) +def translateGPT(text, history, fullPromptFlag, pbar): + mismatch = False + totalTokens = [0, 0] + if isinstance(text, list): + tList = batchList(text, BATCHSIZE) + else: + tList = [text] + + for index, tItem in enumerate(tList): + # Before sending to translation, if we have a list of items, add the formatting + if isinstance(tItem, list): + payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) + payload = re.sub(r"(<)(\/Line\d+>)", r"\1>Placeholder Text<\3", payload) + varResponse = subVars(payload) + subbedT = varResponse[0] + else: + varResponse = subVars(tItem) + subbedT = varResponse[0] + + # Things to Check before starting translation + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): + continue + + # Create Message + characters, system, user = createContext(fullPromptFlag, subbedT) + + # Calculate Estimate + if ESTIMATE: + estimate = countTokens(characters, system, user, history) + totalTokens[0] += estimate[0] + totalTokens[1] += estimate[1] + continue + + # Translating + response = translateText(characters, system, user, history) + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Formatting + translatedText = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedText, True) + tList[index] = extractedTranslations + if len(tItem) != len(extractedTranslations): + # Mismatch. Try Again + response = translateText(characters, system, user, history) + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Formatting + translatedText = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedText, True) + if len(tItem) == len(extractedTranslations): + tList[index] = extractedTranslations + else: + mismatch = True # Just here for breakpoint + + # Create History + history = tList[index] # Update history if we have a list + pbar.update(len(tList[index])) + + else: + # Ensure we're passing a single string to extractTranslation + extractedTranslations = extractTranslation(translatedText, False) + tList[index] = extractedTranslations + + # Combine if multilist + if isinstance(tList[0], list): + tList = [t for sublist in tList for t in sublist] + + # Return + if format == "json": + return [tList, totalTokens] + else: + return [tList[0], totalTokens] diff --git a/modules/json.py b/modules/json.py index 4338e8f..8903a43 100644 --- a/modules/json.py +++ b/modules/json.py @@ -1,568 +1,569 @@ -# Libraries -import json -import os -import re -import textwrap -import threading -import time -import traceback -import tiktoken -import openai -from pathlib import Path -from colorama import Fore -from dotenv import load_dotenv -from retry import retry -from tqdm import tqdm - -# Open AI -load_dotenv() -if os.getenv("api").replace(" ", "") != "": - openai.base_url = os.getenv("api") -openai.organization = os.getenv("org") -openai.api_key = os.getenv("key") - -# Globals -MODEL = os.getenv("model") -TIMEOUT = int(os.getenv("timeout")) -LANGUAGE = os.getenv("language").capitalize() -PROMPT = Path("prompt.txt").read_text(encoding="utf-8") -VOCAB = Path("vocab.txt").read_text(encoding="utf-8") -THREADS = int(os.getenv("threads")) -LOCK = threading.Lock() -WIDTH = int(os.getenv("width")) -LISTWIDTH = int(os.getenv("listWidth")) -NOTEWIDTH = 70 -MAXHISTORY = 10 -ESTIMATE = "" -TOKENS = [0, 0] -NAMESLIST = [] -NAMES = False # Output a list of all the character names found -BRFLAG = False # If the game uses
instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.01 - OUTPUTAPICOST = 0.03 - BATCHSIZE = 50 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleJSON(filename, estimate): - global ESTIMATE, totalTokens - ESTIMATE = estimate - - if estimate: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - return getResultString(["", TOKENS, None], end - start, "TOTAL") - - else: - try: - with open("translated/" + filename, "w", encoding="UTF-8") as outFile: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4) - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - except Exception: - return "Fail" - - return getResultString(["", TOKENS, None], end - start, "TOTAL") - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="UTF-8-sig") as f: - data = json.load(f) - - # Map Files - if ".json" in filename: - translatedData = parseJSON(data, filename) - - else: - raise NameError(filename + " Not Supported") - - return translatedData - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def parseJSON(data, filename): - totalTokens = [0, 0] - totalLines = 0 - totalLines = len(data) - global LOCK - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar: - pbar.desc = filename - pbar.total = totalLines - try: - result = translateJSON(data, pbar) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateJSON(data, pbar): - textHistory = [] - batch = [] - maxHistory = MAXHISTORY - tokens = [0, 0] - speaker = "None" - insertBool = False - i = 0 - batchStartIndex = 0 - - while i < len(data): - item = data[i] - # Speaker - if "VA" in item: - if item["name"] not in [None, "-"]: - response = getSpeaker(item["name"]) - speaker = response[0] - tokens[0] += response[1][0] - tokens[1] += response[1][1] - item["name"] = speaker - else: - speaker = "None" - pbar.update(1) - i += 1 - - # Text - elif "me" in item: - for text in [ - "text", - "text2", - "help1", - "help2", - "help3", - "like", - "message", - "me", - ]: - if text in item: - if item[text] != None: - jaString = item[text] - - # Remove any textwrap - if FIXTEXTWRAP == True: - finalJAString = jaString.replace("\n", " ") - - # [Passthrough 1] Pulling From File - if insertBool is False: - # Append to List and Clear Values - batch.append(finalJAString) - speaker = "" - - # Translate Batch if Full - if len(batch) == BATCHSIZE: - # Translate - response = translateGPT(batch, textHistory, True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - translatedBatch = response[0] - textHistory = translatedBatch[-10:] - - # Set Values - if len(batch) == len(translatedBatch): - i = batchStartIndex - insertBool = True - - # Mismatch - else: - pbar.write(f"Mismatch: {batchStartIndex} - {i}") - MISMATCH.append(batch) - batchStartIndex = i - batch.clear() - - if insertBool is False: - pbar.update(1) - i += 1 - - currentGroup = [] - - # [Passthrough 2] Setting Data - else: - # Get Text - translatedText = translatedBatch[0] - - # Remove added speaker - translatedText = re.sub(r"^.+?:\s", "", translatedText) - - # Textwrap - translatedText = textwrap.fill(translatedText, width=WIDTH) - - # Set Text - item[text] = translatedText - translatedBatch.pop(0) - speaker = "" - currentGroup = [] - i += 1 - - # If Batch is empty. Move on. - if len(translatedBatch) == 0: - insertBool = False - batchStartIndex = i - batch.clear() - else: - i += 1 - pbar.update(1) - - # Translate Batch if not empty and EOF - if len(batch) != 0 and i >= len(data): - # Translate - response = translateGPT(batch, textHistory, True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - translatedBatch = response[0] - textHistory = translatedBatch[-10:] - - # Set Values - if len(batch) == len(translatedBatch): - i = batchStartIndex - insertBool = True - - # Mismatch - else: - pbar.write(f"Mismatch: {batchStartIndex} - {i}") - MISMATCH.append(batch) - batchStartIndex = i - batch.clear() - - currentGroup = [] - return tokens - - -# Save some money and enter the character before translation -def getSpeaker(speaker): - match speaker: - case "ファイン": - return ["Fine", [0, 0]] - case "": - return ["", [0, 0]] - case _: - # Find Speaker - for i in range(len(NAMESLIST)): - if speaker == NAMESLIST[i][0]: - return [NAMESLIST[i][1], [0, 0]] - - # Translate and Store Speaker - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - True, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - response[0] = response[0].replace("Speaker: ", "") - - # Retry if name doesn't translate for some reason - if re.search(r"([a-zA-Z??])", response[0]) == None: - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - - speakerList = [speaker, response[0]] - NAMESLIST.append(speakerList) - return response - return [speaker, [0, 0]] - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT, format): - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - if format == "json": - user = f"```json\n{subbedT}\n```" - else: - user = subbedT - return system, user - - -def translateText(system, user, history, penalty, format, model=MODEL): - # Prompt - msg = [{"role": "system", "content": system}] - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Response Format - if format == "json": - responseFormat = {"type": "json_object"} - else: - responseFormat = {"type": "text"} - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=model, - response_format=responseFormat, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "「": '\\"', - "」": '\\"', - "- ": "-", - "—": "―", - "】": "]", - "【": "[", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Remove Repeating Characters - pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") - translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - try: - translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) - translatedTextList = re.sub(r"(? instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.01 + OUTPUTAPICOST = 0.03 + BATCHSIZE = 50 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleJSON(filename, estimate): + global ESTIMATE, totalTokens + ESTIMATE = estimate + + if estimate: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + return getResultString(["", TOKENS, None], end - start, "TOTAL") + + else: + try: + with open("translated/" + filename, "w", encoding="UTF-8") as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4) + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + except Exception: + return "Fail" + + return getResultString(["", TOKENS, None], end - start, "TOTAL") + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="UTF-8-sig") as f: + data = json.load(f) + + # Map Files + if ".json" in filename: + translatedData = parseJSON(data, filename) + + else: + raise NameError(filename + " Not Supported") + + return translatedData + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def parseJSON(data, filename): + totalTokens = [0, 0] + totalLines = 0 + totalLines = len(data) + global LOCK + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar: + pbar.desc = filename + pbar.total = totalLines + try: + result = translateJSON(data, pbar) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateJSON(data, pbar): + textHistory = [] + batch = [] + maxHistory = MAXHISTORY + tokens = [0, 0] + speaker = "None" + insertBool = False + i = 0 + batchStartIndex = 0 + + while i < len(data): + item = data[i] + # Speaker + if "VA" in item: + if item["name"] not in [None, "-"]: + response = getSpeaker(item["name"]) + speaker = response[0] + tokens[0] += response[1][0] + tokens[1] += response[1][1] + item["name"] = speaker + else: + speaker = "None" + pbar.update(1) + i += 1 + + # Text + elif "me" in item: + for text in [ + "text", + "text2", + "help1", + "help2", + "help3", + "like", + "message", + "me", + ]: + if text in item: + if item[text] != None: + jaString = item[text] + + # Remove any textwrap + if FIXTEXTWRAP == True: + finalJAString = jaString.replace("\n", " ") + + # [Passthrough 1] Pulling From File + if insertBool is False: + # Append to List and Clear Values + batch.append(finalJAString) + speaker = "" + + # Translate Batch if Full + if len(batch) == BATCHSIZE: + # Translate + response = translateGPT(batch, textHistory, True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedBatch = response[0] + textHistory = translatedBatch[-10:] + + # Set Values + if len(batch) == len(translatedBatch): + i = batchStartIndex + insertBool = True + + # Mismatch + else: + pbar.write(f"Mismatch: {batchStartIndex} - {i}") + MISMATCH.append(batch) + batchStartIndex = i + batch.clear() + + if insertBool is False: + pbar.update(1) + i += 1 + + currentGroup = [] + + # [Passthrough 2] Setting Data + else: + # Get Text + translatedText = translatedBatch[0] + + # Remove added speaker + translatedText = re.sub(r"^.+?:\s", "", translatedText) + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + + # Set Text + item[text] = translatedText + translatedBatch.pop(0) + speaker = "" + currentGroup = [] + i += 1 + + # If Batch is empty. Move on. + if len(translatedBatch) == 0: + insertBool = False + batchStartIndex = i + batch.clear() + else: + i += 1 + pbar.update(1) + + # Translate Batch if not empty and EOF + if len(batch) != 0 and i >= len(data): + # Translate + response = translateGPT(batch, textHistory, True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedBatch = response[0] + textHistory = translatedBatch[-10:] + + # Set Values + if len(batch) == len(translatedBatch): + i = batchStartIndex + insertBool = True + + # Mismatch + else: + pbar.write(f"Mismatch: {batchStartIndex} - {i}") + MISMATCH.append(batch) + batchStartIndex = i + batch.clear() + + currentGroup = [] + return tokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + match speaker: + case "ファイン": + return ["Fine", [0, 0]] + case "": + return ["", [0, 0]] + case _: + # Find Speaker + for i in range(len(NAMESLIST)): + if speaker == NAMESLIST[i][0]: + return [NAMESLIST[i][1], [0, 0]] + + # Translate and Store Speaker + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + True, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + response[0] = response[0].replace("Speaker: ", "") + + # Retry if name doesn't translate for some reason + if re.search(r"([a-zA-Z??])", response[0]) == None: + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + False, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + + speakerList = [speaker, response[0]] + NAMESLIST.append(speakerList) + return response + return [speaker, [0, 0]] + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT, format): + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + if format == "json": + user = f"```json\n{subbedT}\n```" + else: + user = subbedT + return system, user + + +def translateText(system, user, history, penalty, format, model=MODEL): + # Prompt + msg = [{"role": "system", "content": system}] + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Response Format + if format == "json": + responseFormat = {"type": "json_object"} + else: + responseFormat = {"type": "text"} + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=model, + response_format=responseFormat, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "「": '\\"', + "」": '\\"', + "- ": "-", + "—": "―", + "】": "]", + "【": "[", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Remove Repeating Characters + pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") + translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + try: + translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) + translatedTextList = re.sub(r"(? instead -FIXTEXTWRAP = False # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.01 - OUTPUTAPICOST = 0.03 - BATCHSIZE = 10 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleKansen(filename, estimate): - global ESTIMATE - ESTIMATE = estimate - - if ESTIMATE: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - else: - try: - with open("translated/" + filename, "w", encoding="shift_jis", errors="ignore") as outFile: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - outFile.writelines(translatedData[0]) - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - except Exception: - traceback.print_exc() - return "Fail" - - return getResultString(["", TOKENS, None], end - start, "TOTAL") - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="cp932") as readFile: - translatedData = parseTyrano(readFile, filename) - - # Delete lines marked for deletion - finalData = [] - for line in translatedData[0]: - if line != "\\d\n": - finalData.append(line) - translatedData[0] = finalData - - return translatedData - - -def parseTyrano(readFile, filename): - totalTokens = [0, 0] - totalLines = 0 - - # Get total for progress bar - data = readFile.readlines() - totalLines = len(data) - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar: - pbar.desc = filename - pbar.total = totalLines - - try: - result = translateTyrano(data, pbar, totalLines) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateTyrano(data, pbar, totalLines): - textHistory = [] - batch = [] - currentGroup = [] - maxHistory = MAXHISTORY - tokens = [0, 0] - speaker = "" - insertBool = False - global LOCK, ESTIMATE - i = 0 - batchStartIndex = 0 - - while i < len(data): - # Speaker - if "[ns]" in data[i]: - matchList = re.findall(r"\[ns\](.+?)\[", data[i]) - if len(matchList) != 0: - response = getSpeaker(matchList[0]) - speaker = response[0] - tokens[0] += response[1][0] - tokens[1] += response[1][1] - data[i] = "[ns]" + speaker + "[nse]\n" - else: - speaker = "" - - # Choices - elif "[sel" in data[i]: - matchList = re.findall(r'\[sel.+text="(.+?)".+', data[i]) - if len(matchList) != 0: - originalText = matchList[0] - if len(textHistory) > 0: - response = translateGPT( - matchList[0], - "Keep your translation as brief as possible. Previous text for context: " - + textHistory[len(textHistory) - 1] - + "\n\nReply in the style of a dialogue option.", - False, - ) - else: - response = translateGPT( - matchList[0], - "\n\nReply in the style of a dialogue option.", - False, - ) - translatedText = response[0] - tokens[0] += response[1][0] - tokens[1] += response[1][1] - - # Remove characters that may break scripts - charList = [".", '"', "\\n"] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Escape all ' - translatedText = translatedText.replace("\\", "") - # translatedText = translatedText.replace("'", "\\\'") - - # Set Data - translatedText = data[i].replace(originalText, translatedText) - data[i] = translatedText - - # Lines - matchList = re.findall(r"(.+?)\[[rpcms_sel]+\]$", data[i]) - if len(matchList) > 0: - if "hisout" in matchList[0]: - i += 1 - continue - currentGroup.append(matchList[0]) - if len(data) > i + 1: - while "[r]" in data[i + 1]: - if insertBool is True: - data[i] = r"\d\n" - pbar.update(1) - i += 1 - matchList = re.findall(r"(.+?)\[r\]", data[i]) - if len(matchList) > 0: - currentGroup.append(matchList[0]) - while "[pcms]" in data[i + 1]: - if insertBool is True: - data[i] = r"\d\n" - pbar.update(1) - i += 1 - matchList = re.findall(r"(.+?)\[pcms\]", data[i]) - if len(matchList) > 0: - currentGroup.append(matchList[0]) - while "[pcms_sel]" in data[i + 1]: - if insertBool is True: - data[i] = r"\d\n" - pbar.update(1) - i += 1 - matchList = re.findall(r"(.+?)\[pcms_sel\]", data[i]) - if len(matchList) > 0: - currentGroup.append(matchList[0]) - # Join up 401 groups for better translation. - if len(currentGroup) > 0: - finalJAString = " ".join(currentGroup) - oldjaString = finalJAString - - # Remove any textwrap - if FIXTEXTWRAP == True: - finalJAString = finalJAString.replace("[r]", " ") - - # Remove Extra Stuff bad for translation. - finalJAString = finalJAString.replace("゙", "") - finalJAString = finalJAString.replace("・", ".") - finalJAString = finalJAString.replace("‶", "") - finalJAString = finalJAString.replace("”", "") - finalJAString = finalJAString.replace("―", "-") - finalJAString = finalJAString.replace("…", "...") - finalJAString = re.sub(r"(\.{3}\.+)", "...", finalJAString) - finalJAString = finalJAString.replace(" ", " ") - - # Furigana Removal - matchList = re.findall(r"(\[ruby\stext=.+text=\"(.+)\"\])", finalJAString) - if len(matchList) > 0: - finalJAString = finalJAString.replace(matchList[0][0], matchList[0][1]) - - # Add Speaker (If there is one) - if speaker != "": - finalJAString = f"{speaker}: {finalJAString}" - - # [Passthrough 1] Pulling From File - if insertBool is False: - # Append to List and Clear Values - batch.append(finalJAString) - speaker = "" - - # Translate Batch if Full - if len(batch) == BATCHSIZE: - # Translate - response = translateGPT(batch, textHistory, True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - translatedBatch = response[0] - textHistory = translatedBatch[-10:] - - # Set Values - if len(batch) == len(translatedBatch): - i = batchStartIndex - insertBool = True - - # Mismatch - else: - pbar.write(f"Mismatch: {batchStartIndex} - {i}") - MISMATCH.append(batch) - batchStartIndex = i - batch.clear() - - i += 1 - if insertBool is True: - pbar.update(1) - currentGroup = [] - - # [Passthrough 2] Setting Data - else: - # Get Text - translatedText = translatedBatch[0] - translatedText = translatedText.replace('\\"', '"') - translatedText = translatedText.replace("[", "(") - translatedText = translatedText.replace("]", ")") - - # Remove added speaker - translatedText = re.sub(r"^.+?:\s", "", translatedText) - - # Textwrap - translatedText = textwrap.fill(translatedText, width=WIDTH) - textList = translatedText.split("\n") - - # Set Text - data[i] = r"\d\n" - for line in textList: - # Wordwrap Text - if "[r]" not in line: - line = textwrap.fill(line, width=WIDTH) - line = line.replace("\n", "[r]") - - # Set - data.insert(i, line.strip() + "[r]\n") - i += 1 - data[i - 1] = data[i - 1].replace("[r]", "[pcms]") - translatedBatch.pop(0) - speaker = "" - currentGroup = [] - - # If Batch is empty. Move on. - if len(translatedBatch) == 0: - insertBool = False - batchStartIndex = i - batch.clear() - - # Nothing relevant. Skip Line. - else: - i += 1 - if insertBool is True: - pbar.update(1) - - # Translate Batch if not empty and EOF - if len(batch) != 0 and i >= len(data): - # Translate - response = translateGPT(batch, textHistory, True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - translatedBatch = response[0] - textHistory = translatedBatch[-10:] - - # Set Values - if len(batch) == len(translatedBatch): - i = batchStartIndex - insertBool = True - - # Mismatch - else: - pbar.write(f"Mismatch: {batchStartIndex} - {i}") - MISMATCH.append(batch) - batchStartIndex = i - batch.clear() - - currentGroup = [] - return tokens - - -# Save some money and enter the character before translation -def getSpeaker(speaker): - match speaker: - case "央": - return ["Akira", [0, 0]] - case "累": - return ["Rui", [0, 0]] - case "梨里": - return ["Riri", [0, 0]] - case "純": - return ["Jun", [0, 0]] - case "美鈴": - return ["Misuzu", [0, 0]] - case "須田": - return ["Suda", [0, 0]] - case "高橋": - return ["Takahashi", [0, 0]] - case "勇二": - return ["Yuuji", [0, 0]] - case _: - return translateGPT( - speaker, - "Reply with only the " + LANGUAGE + " translation of the NPC name.", - False, - ) - - -def subVars(jaString): - jaString = jaString.replace("\u3000", " ") - - # Nested - count = 0 - nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) - nestedList = set(nestedList) - if len(nestedList) != 0: - for icon in nestedList: - jaString = jaString.replace(icon, "{Nested_" + str(count) + "}") - count += 1 - - # Icons - count = 0 - iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) - iconList = set(iconList) - if len(iconList) != 0: - for icon in iconList: - jaString = jaString.replace(icon, "{Ascii_" + str(count) + "}") - count += 1 - - # Colors - count = 0 - colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) - colorList = set(colorList) - if len(colorList) != 0: - for color in colorList: - jaString = jaString.replace(color, "{Color_" + str(count) + "}") - count += 1 - - # Names - count = 0 - nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) - nameList = set(nameList) - if len(nameList) != 0: - for name in nameList: - jaString = jaString.replace(name, "{Noun_" + str(count) + "}") - count += 1 - - # Variables - count = 0 - varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) - varList = set(varList) - if len(varList) != 0: - for var in varList: - jaString = jaString.replace(var, "{Var_" + str(count) + "}") - count += 1 - - # Formatting - count = 0 - formatList = re.findall(r"[\\]+[\w]+\[.+?\]", jaString) - formatList = set(formatList) - if len(formatList) != 0: - for var in formatList: - jaString = jaString.replace(var, "{FCode_" + str(count) + "}") - count += 1 - - # Put all lists in list and return - allList = [nestedList, iconList, colorList, nameList, varList, formatList] - return [jaString, allList] - - -def resubVars(translatedText, allList): - # Fix Spacing and ChatGPT Nonsense - matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) - if len(matchList) > 0: - for match in matchList: - text = match.strip() - translatedText = translatedText.replace(match, text) - - # Nested - count = 0 - if len(allList[0]) != 0: - for var in allList[0]: - translatedText = translatedText.replace("{Nested_" + str(count) + "}", var) - count += 1 - - # Icons - count = 0 - if len(allList[1]) != 0: - for var in allList[1]: - translatedText = translatedText.replace("{Ascii_" + str(count) + "}", var) - count += 1 - - # Colors - count = 0 - if len(allList[2]) != 0: - for var in allList[2]: - translatedText = translatedText.replace("{Color_" + str(count) + "}", var) - count += 1 - - # Names - count = 0 - if len(allList[3]) != 0: - for var in allList[3]: - translatedText = translatedText.replace("{Noun_" + str(count) + "}", var) - count += 1 - - # Vars - count = 0 - if len(allList[4]) != 0: - for var in allList[4]: - translatedText = translatedText.replace("{Var_" + str(count) + "}", var) - count += 1 - - # Formatting - count = 0 - if len(allList[5]) != 0: - for var in allList[5]: - translatedText = translatedText.replace("{FCode_" + str(count) + "}", var) - count += 1 - - return translatedText - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT): - characters = "Game Characters:\n\ -渋江 央 (Shibue Akira) - Male\n\ -蘆名 累 (Ashina Rui) - Female\n\ -清原 梨里 (Kiyohara Riri) - Female\n\ -五十嵐 純 (Igarashi Jun) - Female\n\ -子野日 美鈴 (Nenohi Misuzu) - Female\n\ -須田 (Suda) - Male\n\ -高橋 (Takahashi) - Female\n\ -勇二 (Yuuji) - Male\n\ -" - - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -You are going to be translating text from a videogame.\n\ -I will give you lines of text, and you must translate each line to the best of your ability.\n\ -{VOCAB}\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\ -" - ) - user = f"{subbedT}" - return characters, system, user - - -def translateText(characters, system, user, history): - # Prompt - msg = [{"role": "system", "content": system + characters}] - - # Characters - msg.append({"role": "system", "content": characters}) - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0.1, - frequency_penalty=0.1, - presence_penalty=0.1, - model=MODEL, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - translatedText = resubVars(translatedText, varResponse[1]) - return [line for line in translatedText.replace("\\n", "\n").split("\n") if line] - - -def extractTranslation(translatedTextList, is_list): - pattern = r"`?([\\]*.*?[\\]*?)<\/?Line\d+>`?" - # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. - if is_list: - return [re.findall(pattern, line)[0][1] for line in translatedTextList if re.search(pattern, line)] - else: - matchList = re.findall(pattern, translatedTextList) - return matchList[0][1] if matchList else translatedTextList - - -def countTokens(characters, system, user, history): - inputTotalTokens = 0 - outputTotalTokens = 0 - enc = tiktoken.encoding_for_model("gpt-4") - - # Input - if isinstance(history, list): - for line in history: - inputTotalTokens += len(enc.encode(line)) - else: - inputTotalTokens += len(enc.encode(history)) - inputTotalTokens += len(enc.encode(system)) - inputTotalTokens += len(enc.encode(characters)) - inputTotalTokens += len(enc.encode(user)) - - # Output - outputTotalTokens += round(len(enc.encode(user)) * 3) - - return [inputTotalTokens, outputTotalTokens] - - -@retry(exceptions=Exception, tries=5, delay=5) -def translateGPT(text, history, fullPromptFlag): - totalTokens = [0, 0] - if isinstance(text, list): - tList = batchList(text, BATCHSIZE) - else: - tList = [text] - - for index, tItem in enumerate(tList): - # Before sending to translation, if we have a list of items, add the formatting - if isinstance(tItem, list): - payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) - payload = payload.replace("``", "`Placeholder Text`") - varResponse = subVars(payload) - subbedT = varResponse[0] - else: - varResponse = subVars(tItem) - subbedT = varResponse[0] - - # Things to Check before starting translation - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): - continue - - # Create Message - characters, system, user = createContext(fullPromptFlag, subbedT) - - # Calculate Estimate - if ESTIMATE: - estimate = countTokens(characters, system, user, history) - totalTokens[0] += estimate[0] - totalTokens[1] += estimate[1] - continue - - # Translating - response = translateText(characters, system, user, history) - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Formatting - translatedTextList = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedTextList, True) - tList[index] = extractedTranslations - if len(tItem) != len(translatedTextList): - mismatch = True # Just here so breakpoint can be set - history = extractedTranslations[-10:] # Update history if we have a list - else: - # Ensure we're passing a single string to extractTranslation - extractedTranslations = extractTranslation("\n".join(translatedTextList), False) - tList[index] = extractedTranslations - - # Combine if multilist - if isinstance(tList[0], list): - tList = [t for sublist in tList for t in sublist] - - # Return - if format == "json": - return [tList, totalTokens] - else: - return [tList[0], totalTokens] +# Libraries +import os +import re +import textwrap +import threading +import time +import traceback +import tiktoken +import openai +from pathlib import Path +from colorama import Fore +from dotenv import load_dotenv +from retry import retry +from tqdm import tqdm + +# Open AI +load_dotenv() +if os.getenv("api").replace(" ", "") != "": + openai.base_url = os.getenv("api") +openai.organization = os.getenv("org") +openai.api_key = os.getenv("key") + +# Globals +MODEL = os.getenv("model") +TIMEOUT = int(os.getenv("timeout")) +LANGUAGE = os.getenv("language").capitalize() +PROMPT = Path("prompt.txt").read_text(encoding="utf-8") +VOCAB = Path("vocab.txt").read_text(encoding="utf-8") +THREADS = int(os.getenv("threads")) +LOCK = threading.Lock() +WIDTH = int(os.getenv("width")) +LISTWIDTH = int(os.getenv("listWidth")) +NOTEWIDTH = 70 +MAXHISTORY = 10 +ESTIMATE = "" +TOKENS = [0, 0] +NAMESLIST = [] +NAMES = False # Output a list of all the character names found +BRFLAG = False # If the game uses
instead +FIXTEXTWRAP = False # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.01 + OUTPUTAPICOST = 0.03 + BATCHSIZE = 10 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleKansen(filename, estimate): + global ESTIMATE + ESTIMATE = estimate + + if ESTIMATE: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + else: + try: + with open("translated/" + filename, "w", encoding="shift_jis", errors="ignore") as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + outFile.writelines(translatedData[0]) + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + except Exception: + traceback.print_exc() + return "Fail" + + return getResultString(["", TOKENS, None], end - start, "TOTAL") + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="cp932") as readFile: + translatedData = parseTyrano(readFile, filename) + + # Delete lines marked for deletion + finalData = [] + for line in translatedData[0]: + if line != "\\d\n": + finalData.append(line) + translatedData[0] = finalData + + return translatedData + + +def parseTyrano(readFile, filename): + totalTokens = [0, 0] + totalLines = 0 + + # Get total for progress bar + data = readFile.readlines() + totalLines = len(data) + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar: + pbar.desc = filename + pbar.total = totalLines + + try: + result = translateTyrano(data, pbar, totalLines) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateTyrano(data, pbar, totalLines): + textHistory = [] + batch = [] + currentGroup = [] + maxHistory = MAXHISTORY + tokens = [0, 0] + speaker = "" + insertBool = False + global LOCK, ESTIMATE + i = 0 + batchStartIndex = 0 + + while i < len(data): + # Speaker + if "[ns]" in data[i]: + matchList = re.findall(r"\[ns\](.+?)\[", data[i]) + if len(matchList) != 0: + response = getSpeaker(matchList[0]) + speaker = response[0] + tokens[0] += response[1][0] + tokens[1] += response[1][1] + data[i] = "[ns]" + speaker + "[nse]\n" + else: + speaker = "" + + # Choices + elif "[sel" in data[i]: + matchList = re.findall(r'\[sel.+text="(.+?)".+', data[i]) + if len(matchList) != 0: + originalText = matchList[0] + if len(textHistory) > 0: + response = translateGPT( + matchList[0], + "Keep your translation as brief as possible. Previous text for context: " + + textHistory[len(textHistory) - 1] + + "\n\nReply in the style of a dialogue option.", + False, + ) + else: + response = translateGPT( + matchList[0], + "\n\nReply in the style of a dialogue option.", + False, + ) + translatedText = response[0] + tokens[0] += response[1][0] + tokens[1] += response[1][1] + + # Remove characters that may break scripts + charList = [".", '"', "\\n"] + for char in charList: + translatedText = translatedText.replace(char, "") + + # Escape all ' + translatedText = translatedText.replace("\\", "") + # translatedText = translatedText.replace("'", "\\\'") + + # Set Data + translatedText = data[i].replace(originalText, translatedText) + data[i] = translatedText + + # Lines + matchList = re.findall(r"(.+?)\[[rpcms_sel]+\]$", data[i]) + if len(matchList) > 0: + if "hisout" in matchList[0]: + i += 1 + continue + currentGroup.append(matchList[0]) + if len(data) > i + 1: + while "[r]" in data[i + 1]: + if insertBool is True: + data[i] = r"\d\n" + pbar.update(1) + i += 1 + matchList = re.findall(r"(.+?)\[r\]", data[i]) + if len(matchList) > 0: + currentGroup.append(matchList[0]) + while "[pcms]" in data[i + 1]: + if insertBool is True: + data[i] = r"\d\n" + pbar.update(1) + i += 1 + matchList = re.findall(r"(.+?)\[pcms\]", data[i]) + if len(matchList) > 0: + currentGroup.append(matchList[0]) + while "[pcms_sel]" in data[i + 1]: + if insertBool is True: + data[i] = r"\d\n" + pbar.update(1) + i += 1 + matchList = re.findall(r"(.+?)\[pcms_sel\]", data[i]) + if len(matchList) > 0: + currentGroup.append(matchList[0]) + # Join up 401 groups for better translation. + if len(currentGroup) > 0: + finalJAString = " ".join(currentGroup) + oldjaString = finalJAString + + # Remove any textwrap + if FIXTEXTWRAP == True: + finalJAString = finalJAString.replace("[r]", " ") + + # Remove Extra Stuff bad for translation. + finalJAString = finalJAString.replace("゙", "") + finalJAString = finalJAString.replace("・", ".") + finalJAString = finalJAString.replace("‶", "") + finalJAString = finalJAString.replace("”", "") + finalJAString = finalJAString.replace("―", "-") + finalJAString = finalJAString.replace("…", "...") + finalJAString = re.sub(r"(\.{3}\.+)", "...", finalJAString) + finalJAString = finalJAString.replace(" ", " ") + + # Furigana Removal + matchList = re.findall(r"(\[ruby\stext=.+text=\"(.+)\"\])", finalJAString) + if len(matchList) > 0: + finalJAString = finalJAString.replace(matchList[0][0], matchList[0][1]) + + # Add Speaker (If there is one) + if speaker != "": + finalJAString = f"{speaker}: {finalJAString}" + + # [Passthrough 1] Pulling From File + if insertBool is False: + # Append to List and Clear Values + batch.append(finalJAString) + speaker = "" + + # Translate Batch if Full + if len(batch) == BATCHSIZE: + # Translate + response = translateGPT(batch, textHistory, True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedBatch = response[0] + textHistory = translatedBatch[-10:] + + # Set Values + if len(batch) == len(translatedBatch): + i = batchStartIndex + insertBool = True + + # Mismatch + else: + pbar.write(f"Mismatch: {batchStartIndex} - {i}") + MISMATCH.append(batch) + batchStartIndex = i + batch.clear() + + i += 1 + if insertBool is True: + pbar.update(1) + currentGroup = [] + + # [Passthrough 2] Setting Data + else: + # Get Text + translatedText = translatedBatch[0] + translatedText = translatedText.replace('\\"', '"') + translatedText = translatedText.replace("[", "(") + translatedText = translatedText.replace("]", ")") + + # Remove added speaker + translatedText = re.sub(r"^.+?:\s", "", translatedText) + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + textList = translatedText.split("\n") + + # Set Text + data[i] = r"\d\n" + for line in textList: + # Wordwrap Text + if "[r]" not in line: + line = textwrap.fill(line, width=WIDTH) + line = line.replace("\n", "[r]") + + # Set + data.insert(i, line.strip() + "[r]\n") + i += 1 + data[i - 1] = data[i - 1].replace("[r]", "[pcms]") + translatedBatch.pop(0) + speaker = "" + currentGroup = [] + + # If Batch is empty. Move on. + if len(translatedBatch) == 0: + insertBool = False + batchStartIndex = i + batch.clear() + + # Nothing relevant. Skip Line. + else: + i += 1 + if insertBool is True: + pbar.update(1) + + # Translate Batch if not empty and EOF + if len(batch) != 0 and i >= len(data): + # Translate + response = translateGPT(batch, textHistory, True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedBatch = response[0] + textHistory = translatedBatch[-10:] + + # Set Values + if len(batch) == len(translatedBatch): + i = batchStartIndex + insertBool = True + + # Mismatch + else: + pbar.write(f"Mismatch: {batchStartIndex} - {i}") + MISMATCH.append(batch) + batchStartIndex = i + batch.clear() + + currentGroup = [] + return tokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + match speaker: + case "央": + return ["Akira", [0, 0]] + case "累": + return ["Rui", [0, 0]] + case "梨里": + return ["Riri", [0, 0]] + case "純": + return ["Jun", [0, 0]] + case "美鈴": + return ["Misuzu", [0, 0]] + case "須田": + return ["Suda", [0, 0]] + case "高橋": + return ["Takahashi", [0, 0]] + case "勇二": + return ["Yuuji", [0, 0]] + case _: + return translateGPT( + speaker, + "Reply with only the " + LANGUAGE + " translation of the NPC name.", + False, + ) + + +def subVars(jaString): + jaString = jaString.replace("\u3000", " ") + + # Nested + count = 0 + nestedList = re.findall(r"[\\]+[\w]+\[[\\]+[\w]+\[[0-9]+\]\]", jaString) + nestedList = set(nestedList) + if len(nestedList) != 0: + for icon in nestedList: + jaString = jaString.replace(icon, "{Nested_" + str(count) + "}") + count += 1 + + # Icons + count = 0 + iconList = re.findall(r"[\\]+[iIkKwWaA]+\[[0-9]+\]", jaString) + iconList = set(iconList) + if len(iconList) != 0: + for icon in iconList: + jaString = jaString.replace(icon, "{Ascii_" + str(count) + "}") + count += 1 + + # Colors + count = 0 + colorList = re.findall(r"[\\]+[cC]\[[0-9]+\]", jaString) + colorList = set(colorList) + if len(colorList) != 0: + for color in colorList: + jaString = jaString.replace(color, "{Color_" + str(count) + "}") + count += 1 + + # Names + count = 0 + nameList = re.findall(r"[\\]+[nN]\[.+?\]+", jaString) + nameList = set(nameList) + if len(nameList) != 0: + for name in nameList: + jaString = jaString.replace(name, "{Noun_" + str(count) + "}") + count += 1 + + # Variables + count = 0 + varList = re.findall(r"[\\]+[vV]\[[0-9]+\]", jaString) + varList = set(varList) + if len(varList) != 0: + for var in varList: + jaString = jaString.replace(var, "{Var_" + str(count) + "}") + count += 1 + + # Formatting + count = 0 + formatList = re.findall(r"[\\]+[\w]+\[.+?\]", jaString) + formatList = set(formatList) + if len(formatList) != 0: + for var in formatList: + jaString = jaString.replace(var, "{FCode_" + str(count) + "}") + count += 1 + + # Put all lists in list and return + allList = [nestedList, iconList, colorList, nameList, varList, formatList] + return [jaString, allList] + + +def resubVars(translatedText, allList): + # Fix Spacing and ChatGPT Nonsense + matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) + if len(matchList) > 0: + for match in matchList: + text = match.strip() + translatedText = translatedText.replace(match, text) + + # Nested + count = 0 + if len(allList[0]) != 0: + for var in allList[0]: + translatedText = translatedText.replace("{Nested_" + str(count) + "}", var) + count += 1 + + # Icons + count = 0 + if len(allList[1]) != 0: + for var in allList[1]: + translatedText = translatedText.replace("{Ascii_" + str(count) + "}", var) + count += 1 + + # Colors + count = 0 + if len(allList[2]) != 0: + for var in allList[2]: + translatedText = translatedText.replace("{Color_" + str(count) + "}", var) + count += 1 + + # Names + count = 0 + if len(allList[3]) != 0: + for var in allList[3]: + translatedText = translatedText.replace("{Noun_" + str(count) + "}", var) + count += 1 + + # Vars + count = 0 + if len(allList[4]) != 0: + for var in allList[4]: + translatedText = translatedText.replace("{Var_" + str(count) + "}", var) + count += 1 + + # Formatting + count = 0 + if len(allList[5]) != 0: + for var in allList[5]: + translatedText = translatedText.replace("{FCode_" + str(count) + "}", var) + count += 1 + + return translatedText + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT): + characters = "Game Characters:\n\ +渋江 央 (Shibue Akira) - Male\n\ +蘆名 累 (Ashina Rui) - Female\n\ +清原 梨里 (Kiyohara Riri) - Female\n\ +五十嵐 純 (Igarashi Jun) - Female\n\ +子野日 美鈴 (Nenohi Misuzu) - Female\n\ +須田 (Suda) - Male\n\ +高橋 (Takahashi) - Female\n\ +勇二 (Yuuji) - Male\n\ +" + + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +You are going to be translating text from a videogame.\n\ +I will give you lines of text, and you must translate each line to the best of your ability.\n\ +{VOCAB}\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\ +" + ) + user = f"{subbedT}" + return characters, system, user + + +def translateText(characters, system, user, history): + # Prompt + msg = [{"role": "system", "content": system + characters}] + + # Characters + msg.append({"role": "system", "content": characters}) + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0.1, + frequency_penalty=0.1, + presence_penalty=0.1, + model=MODEL, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + translatedText = resubVars(translatedText, varResponse[1]) + return [line for line in translatedText.replace("\\n", "\n").split("\n") if line] + + +def extractTranslation(translatedTextList, is_list): + pattern = r"`?([\\]*.*?[\\]*?)<\/?Line\d+>`?" + # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. + if is_list: + return [re.findall(pattern, line)[0][1] for line in translatedTextList if re.search(pattern, line)] + else: + matchList = re.findall(pattern, translatedTextList) + return matchList[0][1] if matchList else translatedTextList + + +def countTokens(characters, system, user, history): + inputTotalTokens = 0 + outputTotalTokens = 0 + enc = tiktoken.encoding_for_model("gpt-4") + + # Input + if isinstance(history, list): + for line in history: + inputTotalTokens += len(enc.encode(line)) + else: + inputTotalTokens += len(enc.encode(history)) + inputTotalTokens += len(enc.encode(system)) + inputTotalTokens += len(enc.encode(characters)) + inputTotalTokens += len(enc.encode(user)) + + # Output + outputTotalTokens += round(len(enc.encode(user)) * 3) + + return [inputTotalTokens, outputTotalTokens] + + +@retry(exceptions=Exception, tries=5, delay=5) +def translateGPT(text, history, fullPromptFlag): + totalTokens = [0, 0] + if isinstance(text, list): + tList = batchList(text, BATCHSIZE) + else: + tList = [text] + + for index, tItem in enumerate(tList): + # Before sending to translation, if we have a list of items, add the formatting + if isinstance(tItem, list): + payload = "\n".join([f"`{item}`" for i, item in enumerate(tItem)]) + payload = payload.replace("``", "`Placeholder Text`") + varResponse = subVars(payload) + subbedT = varResponse[0] + else: + varResponse = subVars(tItem) + subbedT = varResponse[0] + + # Things to Check before starting translation + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9]+", subbedT): + continue + + # Create Message + characters, system, user = createContext(fullPromptFlag, subbedT) + + # Calculate Estimate + if ESTIMATE: + estimate = countTokens(characters, system, user, history) + totalTokens[0] += estimate[0] + totalTokens[1] += estimate[1] + continue + + # Translating + response = translateText(characters, system, user, history) + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Formatting + translatedTextList = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedTextList, True) + tList[index] = extractedTranslations + if len(tItem) != len(translatedTextList): + mismatch = True # Just here so breakpoint can be set + history = extractedTranslations[-10:] # Update history if we have a list + else: + # Ensure we're passing a single string to extractTranslation + extractedTranslations = extractTranslation("\n".join(translatedTextList), False) + tList[index] = extractedTranslations + + # Combine if multilist + if isinstance(tList[0], list): + tList = [t for sublist in tList for t in sublist] + + # Return + if format == "json": + return [tList, totalTokens] + else: + return [tList[0], totalTokens] diff --git a/modules/kirikiri.py b/modules/kirikiri.py index de94b09..b2286a1 100644 --- a/modules/kirikiri.py +++ b/modules/kirikiri.py @@ -1,611 +1,612 @@ -# Libraries -import json -import os -import re -import textwrap -import threading -import time -import traceback -import tiktoken -import openai -from pathlib import Path -from colorama import Fore -from dotenv import load_dotenv -from retry import retry -from tqdm import tqdm - -# Open AI -load_dotenv() -if os.getenv("api").replace(" ", "") != "": - openai.base_url = os.getenv("api") -openai.organization = os.getenv("org") -openai.api_key = os.getenv("key") - -# Globals -MODEL = os.getenv("model") -TIMEOUT = int(os.getenv("timeout")) -LANGUAGE = os.getenv("language").capitalize() -PROMPT = Path("prompt.txt").read_text(encoding="utf-8") -VOCAB = Path("vocab.txt").read_text(encoding="utf-8") -THREADS = int(os.getenv("threads")) -LOCK = threading.Lock() -WIDTH = int(os.getenv("width")) -LISTWIDTH = int(os.getenv("listWidth")) -NOTEWIDTH = 70 -MAXHISTORY = 10 -ESTIMATE = "" -TOKENS = [0, 0] -NAMESLIST = [] -NAMES = False # Output a list of all the character names found -BRFLAG = False # If the game uses
instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) -PBAR = None -FILENAME = None - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False - -# Flags -SPEAKERS = True -CHOICES = True -DIALOGUE = True - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 40 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleKirikiri(filename, estimate): - global ESTIMATE, FILENAME - ESTIMATE = estimate - FILENAME = filename - - if ESTIMATE: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - else: - try: - with open("translated/" + filename, "w", encoding="cp932", errors="ignore") as outFile: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - if translatedData[0] != []: - outFile.writelines(translatedData[0]) - else: - PBAR.write(f"{FILENAME} Failed to write") - os.remove(f"translated/{filename}") - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - except Exception: - traceback.print_exc() - os.remove(f"translated/{filename}") - return "Fail" - - return getResultString(["", TOKENS, None], end - start, "TOTAL") - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="cp932") as readFile: - translatedData = parseKiriKiri(readFile, filename) - return translatedData - - -def parseKiriKiri(readFile, filename): - global PBAR - totalTokens = [0, 0] - - # Read File into data - data = readFile.readlines() - - # Create Progress Bar - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as PBAR: - PBAR.desc = filename - - try: - result = translateKiriKiri(data, PBAR, filename, []) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateKiriKiri(data, pbar, filename, jobList): - # Check Job Data - if len(jobList) > 0: - stringList = jobList[0] - choiceList = jobList[1] - setData = True - else: - stringList = [] - choiceList = [] - setData = False - tokens = [0, 0] - speaker = "" - global LOCK, ESTIMATE - i = 0 - - # Regex - speakerRegex = r"【(.*)】\[CR\]" - dialogueRegex = r"^\[text\](.*).*\[KeyWait\]|\[\w+\](.*)\[\/\w+\].*\[KeyWait\]" - furiganaRegex = r'(\[eruby\sstr="(.*?)"\stext.*?\])' - choicesRegex = r"^\s*\[button\d\sclickse=sys_decide.*text='(.*?)'.*" - - while i < len(data): - speaker = "" - # Speaker - match = re.search(speakerRegex, data[i]) - if match and SPEAKERS: - speakerJA = match.group(1) - response = getSpeaker(speakerJA) - speaker = response[0] - tokens[0] += response[1][0] - tokens[1] += response[1][1] - data[i] = data[i].replace(speakerJA, speaker) - i += 1 - - # Choices - match = re.search(choicesRegex, data[i]) - if match and CHOICES: - jaString = match.group(1) - - # Pass 1 - if not setData: - choiceList.append(jaString) - - # Pass 2 - else: - # Grab and Pop and Set - translatedText = choiceList[0] - choiceList.pop(0) - - # Replace Quotes - data[i] = data[i].replace("'", '"') - translatedText = translatedText.replace('"', "'") - data[i] = data[i].replace(jaString, translatedText) - - # Dialogue - match = re.search(dialogueRegex, data[i]) - if match and DIALOGUE: - jaString = match.group(1) - if not jaString: - jaString = match.group(2) - - # Pass 1 - if not setData: - # Remove any textwrap - jaString = jaString.replace("[r]", " ") - - # Remove Furigana - matchList = re.findall(furiganaRegex, jaString) - if matchList: - for match in matchList: - jaString = jaString.replace(match[0], match[1]) - - # Add String - if speaker: - stringList.append(f"[{speaker}]: {jaString.strip()}") - else: - stringList.append(f"{jaString.strip()}") - - # Pass 2 - else: - if len(stringList) > 0: - # Grab and Pop - translatedText = stringList[0] - stringList.pop(0) - - # Remove Speaker - translatedText = re.sub(r"\[.*?\]:\s", "", translatedText) - - # Textwrap - translatedText = textwrap.fill(translatedText, width=WIDTH) - translatedText = translatedText.replace("\n", "[r]") - - # Replace Quotes - data[i] = data[i].replace("'", '"') - translatedText = translatedText.replace('"', "'") - data[i] = data[i].replace(jaString, translatedText) - - # Next Line - i += 1 - - # EOF - stringListTL = [] - choiceListTL = [] - - # Dialogue - if len(stringList) > 0: - # Set Progress - pbar.total = len(stringList) - pbar.refresh() - - # Translate - response = translateGPT( - stringList, - "", - True, - ) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - stringListTL = response[0] - - # Validate - if len(stringList) != len(stringListTL): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - stringListTL = stringList - - # Choices - if len(choiceList) > 0: - # Set Progress - pbar.total = len(choiceList) - pbar.refresh() - - # Translate - response = translateGPT( - choiceList, - "", - True, - ) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - choiceListTL = response[0] - - # Validate - if len(choiceList) != len(choiceListTL): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - choiceListTL = choiceList - - # Proceed to Pass 2 - if not setData: - translateKiriKiri(data, pbar, filename, [stringListTL, choiceListTL]) - - return tokens - - -# Save some money and enter the character before translation -def getSpeaker(speaker): - match speaker: - case "ファイン": - return ["Fine", [0, 0]] - case "": - return ["", [0, 0]] - case _: - # Find Speaker - for i in range(len(NAMESLIST)): - if speaker == NAMESLIST[i][0]: - return [NAMESLIST[i][1], [0, 0]] - - # Translate and Store Speaker - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - True, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - response[0] = response[0].replace("Speaker: ", "") - - # Retry if name doesn't translate for some reason - if re.search(r"([a-zA-Z??])", response[0]) == None: - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - - speakerList = [speaker, response[0]] - NAMESLIST.append(speakerList) - return response - return [speaker, [0, 0]] - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT, format): - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - if format == "json": - user = f"```json\n{subbedT}\n```" - else: - user = subbedT - return system, user - - -def translateText(system, user, history, penalty, format, model=MODEL): - # Prompt - msg = [{"role": "system", "content": system}] - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Response Format - if format == "json": - responseFormat = {"type": "json_object"} - else: - responseFormat = {"type": "text"} - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=model, - response_format=responseFormat, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "「": '\\"', - "」": '\\"', - "- ": "-", - "—": "―", - "】": "]", - "【": "[", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Remove Repeating Characters - pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") - translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - try: - translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) - translatedTextList = re.sub(r"(? instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) +PBAR = None +FILENAME = None + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False + +# Flags +SPEAKERS = True +CHOICES = True +DIALOGUE = True + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 40 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleKirikiri(filename, estimate): + global ESTIMATE, FILENAME + ESTIMATE = estimate + FILENAME = filename + + if ESTIMATE: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + else: + try: + with open("translated/" + filename, "w", encoding="cp932", errors="ignore") as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + if translatedData[0] != []: + outFile.writelines(translatedData[0]) + else: + PBAR.write(f"{FILENAME} Failed to write") + os.remove(f"translated/{filename}") + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + except Exception: + traceback.print_exc() + os.remove(f"translated/{filename}") + return "Fail" + + return getResultString(["", TOKENS, None], end - start, "TOTAL") + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="cp932") as readFile: + translatedData = parseKiriKiri(readFile, filename) + return translatedData + + +def parseKiriKiri(readFile, filename): + global PBAR + totalTokens = [0, 0] + + # Read File into data + data = readFile.readlines() + + # Create Progress Bar + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as PBAR: + PBAR.desc = filename + + try: + result = translateKiriKiri(data, PBAR, filename, []) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateKiriKiri(data, pbar, filename, jobList): + # Check Job Data + if len(jobList) > 0: + stringList = jobList[0] + choiceList = jobList[1] + setData = True + else: + stringList = [] + choiceList = [] + setData = False + tokens = [0, 0] + speaker = "" + global LOCK, ESTIMATE + i = 0 + + # Regex + speakerRegex = r"【(.*)】\[CR\]" + dialogueRegex = r"^\[text\](.*).*\[KeyWait\]|\[\w+\](.*)\[\/\w+\].*\[KeyWait\]" + furiganaRegex = r'(\[eruby\sstr="(.*?)"\stext.*?\])' + choicesRegex = r"^\s*\[button\d\sclickse=sys_decide.*text='(.*?)'.*" + + while i < len(data): + speaker = "" + # Speaker + match = re.search(speakerRegex, data[i]) + if match and SPEAKERS: + speakerJA = match.group(1) + response = getSpeaker(speakerJA) + speaker = response[0] + tokens[0] += response[1][0] + tokens[1] += response[1][1] + data[i] = data[i].replace(speakerJA, speaker) + i += 1 + + # Choices + match = re.search(choicesRegex, data[i]) + if match and CHOICES: + jaString = match.group(1) + + # Pass 1 + if not setData: + choiceList.append(jaString) + + # Pass 2 + else: + # Grab and Pop and Set + translatedText = choiceList[0] + choiceList.pop(0) + + # Replace Quotes + data[i] = data[i].replace("'", '"') + translatedText = translatedText.replace('"', "'") + data[i] = data[i].replace(jaString, translatedText) + + # Dialogue + match = re.search(dialogueRegex, data[i]) + if match and DIALOGUE: + jaString = match.group(1) + if not jaString: + jaString = match.group(2) + + # Pass 1 + if not setData: + # Remove any textwrap + jaString = jaString.replace("[r]", " ") + + # Remove Furigana + matchList = re.findall(furiganaRegex, jaString) + if matchList: + for match in matchList: + jaString = jaString.replace(match[0], match[1]) + + # Add String + if speaker: + stringList.append(f"[{speaker}]: {jaString.strip()}") + else: + stringList.append(f"{jaString.strip()}") + + # Pass 2 + else: + if len(stringList) > 0: + # Grab and Pop + translatedText = stringList[0] + stringList.pop(0) + + # Remove Speaker + translatedText = re.sub(r"\[.*?\]:\s", "", translatedText) + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + translatedText = translatedText.replace("\n", "[r]") + + # Replace Quotes + data[i] = data[i].replace("'", '"') + translatedText = translatedText.replace('"', "'") + data[i] = data[i].replace(jaString, translatedText) + + # Next Line + i += 1 + + # EOF + stringListTL = [] + choiceListTL = [] + + # Dialogue + if len(stringList) > 0: + # Set Progress + pbar.total = len(stringList) + pbar.refresh() + + # Translate + response = translateGPT( + stringList, + "", + True, + ) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + stringListTL = response[0] + + # Validate + if len(stringList) != len(stringListTL): + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + stringListTL = stringList + + # Choices + if len(choiceList) > 0: + # Set Progress + pbar.total = len(choiceList) + pbar.refresh() + + # Translate + response = translateGPT( + choiceList, + "", + True, + ) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + choiceListTL = response[0] + + # Validate + if len(choiceList) != len(choiceListTL): + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + choiceListTL = choiceList + + # Proceed to Pass 2 + if not setData: + translateKiriKiri(data, pbar, filename, [stringListTL, choiceListTL]) + + return tokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + match speaker: + case "ファイン": + return ["Fine", [0, 0]] + case "": + return ["", [0, 0]] + case _: + # Find Speaker + for i in range(len(NAMESLIST)): + if speaker == NAMESLIST[i][0]: + return [NAMESLIST[i][1], [0, 0]] + + # Translate and Store Speaker + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + True, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + response[0] = response[0].replace("Speaker: ", "") + + # Retry if name doesn't translate for some reason + if re.search(r"([a-zA-Z??])", response[0]) == None: + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + False, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + + speakerList = [speaker, response[0]] + NAMESLIST.append(speakerList) + return response + return [speaker, [0, 0]] + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT, format): + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + if format == "json": + user = f"```json\n{subbedT}\n```" + else: + user = subbedT + return system, user + + +def translateText(system, user, history, penalty, format, model=MODEL): + # Prompt + msg = [{"role": "system", "content": system}] + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Response Format + if format == "json": + responseFormat = {"type": "json_object"} + else: + responseFormat = {"type": "text"} + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=model, + response_format=responseFormat, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "「": '\\"', + "」": '\\"', + "- ": "-", + "—": "―", + "】": "]", + "【": "[", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Remove Repeating Characters + pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") + translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + try: + translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) + translatedTextList = re.sub(r"(? instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) -FILENAME = None - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False -PBAR = None - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 20 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - - -def handleRegex(filename, estimate): - global ESTIMATE, TOKENS, FILENAME - ESTIMATE = estimate - FILENAME = filename - - # Translate - start = time.time() - translatedData = openFiles(filename) - - # Translate - if not estimate: - try: - with open("translated/" + filename, "w", encoding="utf-8") as outFile: - outFile.writelines(translatedData[0]) - except Exception: - traceback.print_exc() - return "Fail" - - # Print File - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="utf8") as readFile: - translatedData = parseRegex(readFile, filename) - - # Delete lines marked for deletion - finalData = [] - for line in translatedData[0]: - if line != "\\d\n": - finalData.append(line) - translatedData[0] = finalData - - return translatedData - - -def parseRegex(readFile, filename): - global PBAR - totalTokens = [0, 0] - - # Read File into data - data = readFile.readlines() - - # Create Progress Bar - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - PBAR = pbar - - try: - result = translateRegex(data, []) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateRegex(data, translatedList): - if translatedList: - stringList = translatedList[0] - choiceList = translatedList[1] - else: - stringList = [] - choiceList = [] - tokens = [0, 0] - speaker = "" - global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH - i = 0 - - while i < len(data): - voice = False - lineRegexText = r"t\s'(.*)'$" - lineRegexSpeaker = r"n\s'(.*)'$" - choiceRegex = r"choice:\d+\s'(.*)'" - titleRegex = r"title\s'(.*)'$" - - # Title - match = re.search(titleRegex, data[i]) - if match: - response = translateGPT( - match.group(1), - f"Reply with the {LANGUAGE} translation of the chapter title", - True, - ) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - title = response[0] - - # Set - if not translatedList: - title = re.sub(r"(?`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - if format == "json": - user = f"```json\n{subbedT}\n```" - else: - user = subbedT - return system, user - - -def translateText(system, user, history, penalty, format, model=MODEL): - # Prompt - msg = [{"role": "system", "content": system}] - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Response Format - if format == "json": - responseFormat = {"type": "json_object"} - else: - responseFormat = {"type": "text"} - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=model, - response_format=responseFormat, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "「": '\\"', - "」": '\\"', - "- ": "-", - "—": "―", - "】": "]", - "【": "[", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Remove Repeating Characters - pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") - translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - try: - translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) - translatedTextList = re.sub(r"(? instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) +FILENAME = None + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False +PBAR = None + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 20 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleRegex(filename, estimate): + global ESTIMATE, TOKENS, FILENAME + ESTIMATE = estimate + FILENAME = filename + + # Translate + start = time.time() + translatedData = openFiles(filename) + + # Translate + if not estimate: + try: + with open("translated/" + filename, "w", encoding="cp932") as outFile: + outFile.writelines(translatedData[0]) + except Exception: + traceback.print_exc() + return "Fail" + + # Print File + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="cp932") as readFile: + translatedData = parseRegex(readFile, filename) + + # Delete lines marked for deletion + finalData = [] + for line in translatedData[0]: + if line != "\\d\n": + finalData.append(line) + translatedData[0] = finalData + + return translatedData + + +def parseRegex(readFile, filename): + global PBAR + totalTokens = [0, 0] + + # Read File into data + data = readFile.readlines() + + # Create Progress Bar + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + PBAR = pbar + + try: + result = translateRegex(data, []) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateRegex(data, translatedList): + if translatedList: + stringList = translatedList[0] + choiceList = translatedList[1] + else: + stringList = [] + choiceList = [] + tokens = [0, 0] + speaker = "" + global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH + i = 0 + + while i < len(data): + voice = False + lineRegexText = r"t\s'(.*)'$" + lineRegexSpeaker = r"n\s'(.*)'$" + choiceRegex = r"\$menu_item.+?,(.*?)," + titleRegex = r"title\s'(.*)'$" + + # Title + match = re.search(titleRegex, data[i]) + if match: + response = translateGPT( + match.group(1), + f"Reply with the {LANGUAGE} translation of the chapter title", + True, + ) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + title = response[0] + + # Set + if not translatedList: + title = re.sub(r"(?`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + if format == "json": + user = f"```json\n{subbedT}\n```" + else: + user = subbedT + return system, user + + +def translateText(system, user, history, penalty, format, model=MODEL): + # Prompt + msg = [{"role": "system", "content": system}] + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Response Format + if format == "json": + responseFormat = {"type": "json_object"} + else: + responseFormat = {"type": "text"} + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=model, + response_format=responseFormat, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "「": '\\"', + "」": '\\"', + "- ": "-", + "—": "―", + "】": "]", + "【": "[", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Remove Repeating Characters + pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") + translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + try: + translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) + translatedTextList = re.sub(r"(? instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) -PBAR = None -FILENAME = None - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 - FREQUENCY_PENALTY = 0.2 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 30 - FREQUENCY_PENALTY = 0.1 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False - -# Dialogue / Scroll / Choices (Main Codes) -CODE401 = True -CODE405 = True -CODE102 = True - -# Optional -CODE101 = True # Turn this one when names exist in 101 -CODE408 = False # Warning, translates comments and can inflate costs. - -# Variables -CODE122 = False - -# Other -CODE355655 = False -CODE357 = False -CODE657 = False -CODE356 = False -CODE320 = False -CODE324 = False -CODE111 = False -CODE108 = False - - -def handleMVMZ(filename, estimate): - global ESTIMATE, TOKENS, FILENAME - ESTIMATE = estimate - FILENAME = filename - - # Translate - start = time.time() - translatedData = openFiles(filename) - - # Translate - if not estimate: - try: - with open("translated/" + filename, "w", encoding="utf-8") as outFile: - json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4) - except Exception: - traceback.print_exc() - return "Fail" - - # Print File - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="utf-8-sig") as f: - data = json.load(f) - - # Map Files - if "Map" in filename and filename != "MapInfos.json": - translatedData = parseMap(data, filename) - - # CommonEvents Files - elif "CommonEvents" in filename: - translatedData = parseCommonEvents(data, filename) - - # Actor File - elif "Actors" in filename: - translatedData = parseNames(data, filename, "Actors") - - # Armor File - elif "Armors" in filename: - translatedData = parseNames(data, filename, "Armors") - - # Weapons File - elif "Weapons" in filename: - translatedData = parseNames(data, filename, "Weapons") - - # Classes File - elif "Classes" in filename: - translatedData = parseNames(data, filename, "Classes") - - # Enemies File - elif "Enemies" in filename: - translatedData = parseNames(data, filename, "Enemies") - - # Items File - elif "Items" in filename: - translatedData = parseNames(data, filename, "Items") - - # MapInfo File - elif "MapInfos" in filename: - translatedData = parseNames(data, filename, "MapInfos") - - # Skills File - elif "Skills" in filename: - translatedData = parseNames(data, filename, "Skills") - - # Troops File - elif "Troops" in filename: - translatedData = parseTroops(data, filename) - - # States File - elif "States" in filename: - translatedData = parseSS(data, filename) - - # System File - elif "System" in filename: - translatedData = parseSystem(data, filename) - - # Scenario File - elif "Scenario" in filename: - translatedData = parseScenario(data, filename) - - else: - raise NameError(filename + " Not Supported") - - return translatedData - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] is None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def parseMap(data, filename): - totalTokens = [0, 0] - totalLines = 0 - events = data["events"] - global LOCK - - # Translate displayName for Map files - if "Map" in filename: - response = translateGPT( - data["displayName"], - "Reply with only the " + LANGUAGE + " translation of the RPG location name", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - data["displayName"] = response[0].replace('"', "") - - # Get total for progress bar - for event in events: - if event is not None: - for page in event["pages"]: - totalLines += len(page["list"]) - - # Thread for each page in file - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - with ThreadPoolExecutor(max_workers=THREADS) as executor: - for event in events: - if event is not None: - # This translates ID of events. (May break the game) - if ".*") - totalTokens[0] += response[0] - totalTokens[1] += response[1] - if ".*") - totalTokens[0] += response[0] - totalTokens[1] += response[1] - if ".*") - totalTokens[0] += response[0] - totalTokens[1] += response[1] - - futures = [executor.submit(searchCodes, page, pbar, [], filename) for page in event["pages"] if page is not None] - for future in as_completed(futures): - try: - totalTokensFuture = future.result() - totalTokens[0] += totalTokensFuture[0] - totalTokens[1] += totalTokensFuture[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateNote(event, regex): - # Regex String - jaString = event["note"] - match = re.findall(regex, jaString, re.DOTALL) - if match: - tokens = [0, 0] - i = 0 - while i < len(match): - initialJAString = match[i] - # Remove any textwrap - modifiedJAString = initialJAString.replace("\n", " ") - - # Translate - response = translateGPT( - modifiedJAString, - "Reply with only the " + LANGUAGE + " translation.", - False, - ) - translatedText = response[0] - tokens[0] += response[1][0] - tokens[1] += response[1][1] - - # Textwrap - translatedText = textwrap.fill(translatedText, width=NOTEWIDTH) - translatedText = translatedText.replace('"', "") - jaString = jaString.replace(initialJAString, translatedText) - event["note"] = jaString - i += 1 - return tokens - return [0, 0] - - -# For notes that can't have spaces. -def translateNoteOmitSpace(event, regex): - # Regex that only matches text inside LB. - jaString = event["note"] - - match = re.findall(regex, jaString, re.DOTALL) - if match: - oldJAString = match[0] - # Remove any textwrap - jaString = re.sub(r"\n", " ", oldJAString) - - # Translate - response = translateGPT( - jaString, - "Reply with the " + LANGUAGE + " translation of the location name.", - False, - ) - translatedText = response[0] - - translatedText = translatedText.replace('"', "") - translatedText = translatedText.replace(" ", "_") - event["note"] = event["note"].replace(oldJAString, translatedText) - return response[1] - return [0, 0] - - -def parseCommonEvents(data, filename): - totalTokens = [0, 0] - totalLines = 0 - global LOCK - - # Get total for progress bar - for page in data: - if page is not None: - totalLines += len(page["list"]) - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - with ThreadPoolExecutor(max_workers=THREADS) as executor: - futures = [executor.submit(searchCodes, page, pbar, [], filename) for page in data if page is not None] - for future in as_completed(futures): - try: - totalTokensFuture = future.result() - totalTokens[0] += totalTokensFuture[0] - totalTokens[1] += totalTokensFuture[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def parseTroops(data, filename): - totalTokens = [0, 0] - totalLines = 0 - global LOCK - - # Get total for progress bar - for troop in data: - if troop is not None: - for page in troop["pages"]: - totalLines += len(page["list"]) + 1 # The +1 is because each page has a name. - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - for troop in data: - if troop is not None: - with ThreadPoolExecutor(max_workers=THREADS) as executor: - futures = [executor.submit(searchCodes, page, pbar, [], filename) for page in troop["pages"] if page is not None] - for future in as_completed(futures): - try: - totalTokensFuture = future.result() - totalTokens[0] += totalTokensFuture[0] - totalTokens[1] += totalTokensFuture[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def parseNames(data, filename, context): - totalTokens = [0, 0] - totalLines = 0 - totalLines += len(data) - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - try: - result = searchNames(data, pbar, context) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def parseSS(data, filename): - totalTokens = [0, 0] - totalLines = 0 - totalLines += len(data) - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - for ss in data: - if ss is not None: - try: - result = searchSS(ss, pbar) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def parseSystem(data, filename): - totalTokens = [0, 0] - totalLines = 0 - - # Calculate Total Lines - for term in data["terms"]: - termList = data["terms"][term] - totalLines += len(termList) - totalLines += len(data["gameTitle"]) - totalLines += len(data["terms"]["messages"]) - totalLines += len(data["variables"]) - totalLines += len(data["equipTypes"]) - totalLines += len(data["armorTypes"]) - totalLines += len(data["skillTypes"]) - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - try: - result = searchSystem(data, pbar) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def parseScenario(data, filename): - totalTokens = [0, 0] - totalLines = 0 - global LOCK - - # Get total for progress bar - for page in data.items(): - totalLines += len(page[1]) - - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - with ThreadPoolExecutor(max_workers=THREADS) as executor: - futures = [executor.submit(searchCodes, page[1], pbar, [], filename) for page in data.items() if page[1] is not None] - for future in as_completed(futures): - try: - totalTokensFuture = future.result() - totalTokens[0] += totalTokensFuture[0] - totalTokens[1] += totalTokensFuture[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def searchNames(data, pbar, context): - totalTokens = [0, 0] - nameList = [] - profileList = [] - nicknameList = [] - descriptionList = [] - noteList = [] - i = 0 # Counter - j = 0 # Counter 2 - filling = False - mismatch = False - batchFull = False - - # Set the context of what we are translating - if "Actors" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the NPC name" - if "Armors" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the RPG equipment name" - if "Classes" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the RPG class name" - if "MapInfos" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the location name" - if "Enemies" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the enemy NPC name" - if "Weapons" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the RPG weapon name" - if "Items" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the RPG item name" - if "Skills" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the RPG skill name" - - # Names - with open("translations.txt", "a", encoding="utf-8") as file: - file.write(f"\n#{context}\n") - while i < len(data) or filling == True: - if i < len(data): - # Empty Data - if data[i] is None or data[i]["name"] == "": - i += 1 - - continue - - # Filling up Batch - filling = True - if context in "Actors": - if len(nameList) < BATCHSIZE: - if data[i]["name"] != "": - nameList.append(data[i]["name"]) - if data[i]["nickname"] != "": - nicknameList.append(data[i]["nickname"]) - if data[i]["profile"] != "": - profileList.append(data[i]["profile"].replace("\n", " ")) - - # Notes - if "") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - if "PE拡張" in data[i]["note"]: - tokensResponse = translateNote(data[i], r"") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - i += 1 - else: - batchFull = True - if context in ["Armors", "Weapons", "Items"]: - if len(nameList) < BATCHSIZE: - nameList.append(data[i]["name"]) - if "description" in data[i] and data[i]["description"] != "": - descriptionList.append(data[i]["description"].replace("\n", " ")) - if "") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - if "") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - if "") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - if "") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - if "") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - if "") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - if "") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - if "Switch Shop Description" in data[i]["note"]: - tokensResponse = translateNote(data[i], r"\n(.*)\n") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - if "") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - - i += 1 - else: - batchFull = True - if context in ["Skills"]: - if len(nameList) < BATCHSIZE: - nameList.append(data[i]["name"]) - if "description" in data[i] and data[i]["description"]: - descriptionList.append(data[i]["description"].replace("\n", " ")) - - # Messages - number = 1 - while number < 5: - if f"message{number}" in data[i] and data[i][f"message{number}"]: - if data[i][f"message{number}"][0] in ["は", "を", "の", "に", "が"]: - msgResponse = translateGPT( - "Taro" + data[i][f"message{number}"], - "reply with only the gender neutral " - + LANGUAGE - + " translation of the action log. Always start the sentence with Taro. For example, Translate 'Taroを倒した!' as 'Taro was defeated!'", - False, - ) - data[i][f"message{number}"] = msgResponse[0].replace("Taro", "") - totalTokens[0] += msgResponse[1][0] - totalTokens[1] += msgResponse[1][1] - number += 1 - - else: - msgResponse = translateGPT( - data[i][f"message{number}"], - "reply with only the gender neutral " + LANGUAGE + " translation", - False, - ) - data[i][f"message{number}"] = msgResponse[0] - totalTokens[0] += msgResponse[1][0] - totalTokens[1] += msgResponse[1][1] - number += 1 - else: - number += 1 - - i += 1 - else: - batchFull = True - if context in ["Enemies", "Classes", "MapInfos"]: - if len(nameList) < BATCHSIZE: - nameList.append(data[i]["name"]) - - # Notes - if "note" in data[i]: - if "") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - if "") - totalTokens[0] += tokensResponse[0] - totalTokens[1] += tokensResponse[1] - i += 1 - else: - batchFull = True - - # Batch Full - if batchFull == True or i >= len(data): - k = j # Original Index - if context in "Actors": - # Name - response = translateGPT(nameList, newContext, True) - translatedNameBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Nickname - if nicknameList: - response = translateGPT(nicknameList, newContext, True) - translatedNicknameBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Profile - if profileList: - response = translateGPT(profileList, "", True) - translatedProfileBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Data - if len(nameList) == len(translatedNameBatch): - j = k - while j < i: - # Empty Data - if data[j] is None or data[j]["name"] == "": - j += 1 - continue - else: - # Get Text - if data[j]["name"] != "": - with open("translations.txt", "a", encoding="utf-8") as file: - file.write(f'{data[j]["name"]} ({translatedNameBatch[0]})\n') - data[j]["name"] = translatedNameBatch[0] - translatedNameBatch.pop(0) - if data[j]["nickname"] != "": - data[j]["nickname"] = translatedNicknameBatch[0] - translatedNicknameBatch.pop(0) - if data[j]["profile"] != "": - data[j]["profile"] = textwrap.fill(translatedProfileBatch[0], LISTWIDTH) - translatedProfileBatch.pop(0) - - # If Batch is empty. Move on. - if len(translatedNameBatch) == 0: - nameList.clear() - filling = False - j += 1 - else: - mismatch = True - - if context in ["Armors", "Weapons", "Items", "Skills"]: - # Name - response = translateGPT(nameList, newContext, True) - translatedNameBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Description - if descriptionList: - response = translateGPT( - descriptionList, - f"Reply with only the {LANGUAGE} translation of the text.", - True, - ) - translatedDescriptionBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Data - if len(nameList) == len(translatedNameBatch): - j = k - with open("translations.txt", "a", encoding="utf-8") as file: - while j < i: - # Empty Data - if data[j] is None or data[j]["name"] == "": - j += 1 - continue - else: - # Get Text - file.write(f'{data[j]['name']} ({translatedNameBatch[0]})\n') - data[j]["name"] = translatedNameBatch[0] - translatedNameBatch.pop(0) - if "description" in data[j] and data[j]["description"] != "": - data[j]["description"] = textwrap.fill(translatedDescriptionBatch[0], LISTWIDTH) - translatedDescriptionBatch.pop(0) - - # If Batch is empty. Move on. - if len(translatedNameBatch) == 0: - nameList.clear() - descriptionList.clear() - batchFull = False - filling = False - j += 1 - else: - mismatch = True - if context in ["Enemies", "Classes", "MapInfos"]: - response = translateGPT(nameList, newContext, True) - translatedNameBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Data - if len(nameList) == len(translatedNameBatch): - j = k - while j < i: - # Empty Data - if data[j] is None or data[j]["name"] == "": - j += 1 - continue - else: - with open("translations.txt", "a", encoding="utf-8") as file: - file.write(f'{data[j]["name"]} ({translatedNameBatch[0]})\n') - # Get Text - data[j]["name"] = translatedNameBatch[0] - translatedNameBatch.pop(0) - - # If Batch is empty. Move on. - if len(translatedNameBatch) == 0: - nameList.clear() - batchFull = False - filling = False - j += 1 - else: - mismatch = True - - # Mismatch - if mismatch == True: - MISMATCH.append(nameList) - nameList.clear() - profileList.clear() - descriptionList.clear() - filling = False - mismatch = False - - i += 1 - - return totalTokens - - -def searchCodes(page, pbar, jobList, filename): - if len(jobList) > 0: - list401 = jobList[0] - list122 = jobList[1] - list355655 = jobList[2] - list108 = jobList[3] - list356 = jobList[4] - list357 = jobList[5] - setData = True - else: - list401 = [] - list122 = [] - list355655 = [] - list108 = [] - list356 = [] - list357 = [] - setData = False - textHistory = [] - match = [] - totalTokens = [0, 0] - translatedText = "" - speaker = "" - speakerID = None - syncIndex = 0 - CLFlag = False - maxHistory = MAXHISTORY - VNameValue = None - speakerWindow = FIRSTLINESPEAKERS - global LOCK - global NAMESLIST - global MISMATCH - global PBAR - with LOCK: - PBAR = pbar - - # Begin Parsing File - try: - # Normal Format - if "list" in page: - codeList = page["list"] - - # Special Format (Scenario) - else: - codeList = page - - # Iterate through page - i = 0 - while i < len(codeList): - with LOCK: - # syncIndex will keep i in sync when it gets modified - if syncIndex > i: - i = syncIndex - if len(codeList) <= i: - break - - # Declare Varss - currentGroup = [] - nametag = "" - - ## Event Code: 401 Show Text - if "code" in codeList[i] and codeList[i]["code"] in [401, 405, -1] and (CODE401 or CODE405): - # Save Code and starting index (j) - code = codeList[i]["code"] - j = i - endtag = "" - - # Grab String - if len(codeList[i]["parameters"]) > 0: - jaString = codeList[i]["parameters"][0] - oldjaString = jaString - else: - codeList[i]["code"] = -1 - i += 1 - continue - - # Validate Japanese Text - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", jaString) and IGNORETLTEXT: - i += 1 - continue - - # # For Retarded Devs - # retardRegex = r'([\\]+[nN]\[[\\]+V\[\d*?\]\])' - # match = re.search(retardRegex, jaString) - # if match: - # if VNameValue == 1: - # jaString = re.sub(retardRegex, 'リッカ', jaString) - # if VNameValue == 2: - # jaString = re.sub(retardRegex, 'ミミ', jaString) - # if VNameValue == 3: - # jaString = re.sub(retardRegex, 'ヒトミ', jaString) - # if VNameValue == 4: - # jaString = re.sub(retardRegex, 'Taro', jaString) - # if VNameValue == 5: - # jaString = re.sub(retardRegex, '富士見', jaString) - - # Speaker Check - speakerList = [] - - # m and z Codes - match = re.search(r"(.*?)[\\]+m\[\d+?\][\\]+z\[\d+?\]", jaString) - if match: - speakerList.append(match.group(1)) - if "\\c" in speakerList[0]: - speakerList = re.findall( - r"^[\\]+[cC]\[[\d]+\]【?(.+?)】?[\\]+[Cc]\[[\d]\]\\?\\?$", - speakerList[0], - ) - - # Brackets - if len(speakerList) == 0: - speakerList = re.findall(r"^【(.*?)】$", jaString) - - # Colors - if len(speakerList) == 0: - speakerList = re.findall( - r"^[\\]+[cC]\[[\d]+\]【?(.+?)】?[\\]+[Cc]\[[\d]\]\\?\\?$", - jaString, - ) - - # First Line Speakers - if len(speakerList) == 0 and FIRSTLINESPEAKERS is True: - # Remove any RPGMaker Code at start - ffMatch = re.search( - r"^(\s*[\\]+[aAbBdDeEfFgGhHjJlLmMoOpPqQrRsStTuUwWxXyYzZ]+\[[\w\d\[\]\\]+\])", - jaString, - ) - if ffMatch != None: - jaString = jaString.replace(ffMatch.group(0), "") - nametag += ffMatch.group(0) - - # Test Speaker - if ( - len(jaString) < 40 - and "code" in codeList[i + 1] - and codeList[i + 1]["code"] in [401, 405, -1] - and len(codeList[i + 1]["parameters"]) > 0 - and len(codeList[i + 1]["parameters"][0]) > 0 - ): - if codeList[i + 1]["parameters"] != "" and codeList[i + 1]["parameters"][0].strip()[0] in [ - "「", - '"', - "(", - "(", - "*", - "[", - ]: - speakerList = re.findall(r".+", jaString) - - if len(speakerList) != 0 and codeList[i + 1]["code"] in [401, 405, -1]: - # Get Speaker - response = getSpeaker(speakerList[0]) - speaker = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Data - codeList[i]["parameters"][0] = nametag + jaString.replace(speakerList[0], speaker) - nametag = "" - - # Iterate to next string - i += 1 - j = i - while codeList[i]["code"] in [-1]: - i += 1 - j = i - jaString = codeList[i]["parameters"][0] - - # Using this to keep track of 401's in a row. - currentGroup.append(jaString) - - # Join Up 401's into single string - if len(codeList) > i + 1: - while codeList[i + 1]["code"] in [401, 405, -1]: - if setData == True: - codeList[i]["parameters"] = [] - codeList[i]["code"] = -1 - i += 1 - j = i - - # Only add if not empty - if len(codeList[i]["parameters"]) > 0: - jaString = codeList[i]["parameters"][0] - currentGroup.append(jaString) - - # Make sure not the end of the list. - if len(codeList) <= i + 1: - break - - # Format String - if len(currentGroup) > 0: - finalJAString = "\n".join(currentGroup) - oldjaString = finalJAString - - # Check if Empty - if finalJAString == "": - i += 1 - continue - - # Set Back - if setData == True: - codeList[i]["parameters"] = [finalJAString] - - ### \\n - nCase = None - regex = r"([\\]+[kKnN][wWcCrRrEe]?[\[<](.*?)[>\]])" - match = re.search(regex, finalJAString) - - # Set Name - if match: - nametag = match.group(1) - speaker = match.group(2) - - # Translate Speaker - response = getSpeaker(speaker) - tledSpeaker = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Nametag and Remove from Final String - finalJAString = finalJAString.replace(nametag, "") - nametag = nametag.replace(speaker, tledSpeaker) - speaker = tledSpeaker - - # Remove Extra Stuff bad for translation. - finalJAString = finalJAString.replace("゙", "") - finalJAString = finalJAString.replace("―", "-") - finalJAString = finalJAString.replace("…", "...") - finalJAString = finalJAString.replace("。", ".") - finalJAString = re.sub(r"(\.{3}\.+)", "...", finalJAString) - finalJAString = finalJAString.replace(" ", "") - finalJAString = finalJAString.replace("「", '"') - finalJAString = finalJAString.replace("」", '"') - - ### Remove format codes - # Furigana - rcodeMatch = re.findall(r"([\\]+[r][b]?\[.*?,(.*?)\])", finalJAString) - if len(rcodeMatch) > 0: - for match in rcodeMatch: - finalJAString = finalJAString.replace(match[0], match[1]) - - # Formatting - formatMatch = re.findall(r"[\\]+[!><.|#^{}]", finalJAString) - if len(formatMatch) > 0: - for match in formatMatch: - finalJAString = finalJAString.replace(match, "") - - # Remove any RPGMaker Code at start - ffMatch = re.search( - r"^(\s*[\\]+[aAbBdDeEfFgGhHjJlLmMoOpPqQrRsStTuUwWxXyYzZ]+\[[\w\d\[\]\\]+?\])", - finalJAString, - ) - if ffMatch != None: - finalJAString = finalJAString.replace(ffMatch.group(1), "") - nametag += ffMatch.group(1) - - # Remove _ABL Codes - ffMatch = re.search(r"^(_ABL).*", finalJAString) - if ffMatch != None: - finalJAString = finalJAString.replace(ffMatch.group(1), "") - nametag += ffMatch.group(1) - - # Center Lines - if "\\CL" in finalJAString or "\\ac" in finalJAString: - finalJAString = finalJAString.replace("\\CL ", "") - finalJAString = finalJAString.replace("\\CL", "") - finalJAString = finalJAString.replace("\\ac ", "") - finalJAString = finalJAString.replace("\\ac", "") - CLFlag = True - - # 1st Passthrough (Grabbing Data) - if setData == False: - # Remove Textwrap - if FIXTEXTWRAP: - finalJAString = finalJAString.replace("\n", " ") - if "\\px[200]" in finalJAString: - finalJAString = finalJAString.replace("\\px[200]", "") - - # Append - if finalJAString != "": - if speaker == "" and finalJAString != "": - list401.append(finalJAString) - elif finalJAString != "": - list401.append(f"[{speaker}]: {finalJAString}") - else: - list401.append(speaker) - speaker = "" - match = [] - nametag = "" - currentGroup = [] - syncIndex = i + 1 - - # Keep textHistory list at length maxHistory - textHistory.append('"' + finalJAString + '"') - if len(textHistory) > maxHistory: - textHistory.pop(0) - - # 2nd Passthrough (Setting Data) - else: - # Grab Translated String - if len(list401) > 0: - translatedText = list401[0] - - # Remove speaker - if speaker != "": - matchSpeakerList = re.findall(r"^\[?(.+?)\]?\s?[|:]\s?", translatedText) - if len(matchSpeakerList) > 0: - newSpeaker = matchSpeakerList[0] - nametag = nametag.replace(speaker, newSpeaker) - translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText) - - # Fix '- ' - translatedText = translatedText.replace("- ", "-") - - # Textwrap - if FIXTEXTWRAP is True: - finalJAString = re.sub(r"\n", " ", finalJAString) - finalJAString = finalJAString.replace("
", " ") - - if FIXTEXTWRAP is True and "_ABL" in nametag: - translatedText = textwrap.fill(translatedText, width=100) - elif FIXTEXTWRAP is True: - translatedText = textwrap.fill(translatedText, width=WIDTH) - - # BR Flag - if BRFLAG is True: - translatedText = translatedText.replace("\n", "
") - - # px - if "\\px[200]" in nametag: - translatedText = translatedText.replace("\\px[200]", "") - translatedText = translatedText.replace("\n", "\n\\px[200]") - - ### Add Var Strings - # CL Flag - if CLFlag: - translatedText = "\\ac " + translatedText - translatedText = translatedText.replace("\n", "\n\\ac ") - translatedText = re.sub(r"[\\]+?ac\s+", r"\\ac ", translatedText) - CLFlag = False - - # Nametag - if nCase == 0: - translatedText = translatedText + nametag - else: - translatedText = nametag + translatedText - nametag = "" - - # Endtag - if endtag != "": - translatedText = translatedText + endtag - endtag = "" - - # Set Data - codeList[j]["parameters"] = [translatedText] - codeList[j]["code"] = code - speaker = "" - match = [] - currentGroup = [] - syncIndex = i + 1 - list401.pop(0) - - ## Event Code: 122 [Set Variables] - if "code" in codeList[i] and codeList[i]["code"] == 122 and CODE122 is True: - # This is going to be the var being set. (IMPORTANT) - if codeList[i]["parameters"][0] not in list(range(42, 45)): - i += 1 - continue - - jaString = codeList[i]["parameters"][4] - - # # For Retarded Devs - # VNameValue = jaString - # i += 1 - # continue - - # Definitely don't want to mess with files - # if 'gameV' in jaString or '_' in jaString: - # i += 1 - # continue - - # Validate String - if not isinstance(jaString, str): - i += 1 - continue - - # Set String - matchedText = None - if len(re.findall(r"([\'\"\`])", jaString)) >= 2: - matchedText = re.search(r"[\'\"\`](.*)[\'\"\`]", jaString) - # else: - # matchedText = re.search(r'(.*)', jaString) - - # Last Check - if matchedText.group(1) != None and matchedText.group(1) != " ": - # Remove Textwrap - finalJAString = matchedText.group(1).replace("\\n", " ") - - # Pass 1 - if setData == False: - if finalJAString != "": - list122.append(finalJAString) - - # Pass 2 - else: - if len(list122) > 0: - # Grab and Replace - translatedText = list122[0] - translatedText = jaString.replace(jaString, translatedText) - - # Remove characters that may break scripts - charList = ['"', "\\n"] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Textwrap - translatedText = textwrap.fill(translatedText, width=WIDTH) - translatedText = translatedText.replace("\n", "\\n") - - # Set - codeList[i]["parameters"][4] = f"`{translatedText}`" - list122.pop(0) - - ## Event Code: 357 [Picture Text] [Optional] - if "code" in codeList[i] and codeList[i]["code"] == 357 and CODE357 is True: - headerString = codeList[i]["parameters"][0] - - if headerString == "LL_GalgeChoiceWindow": - ### Message Text First - jaString = codeList[i]["parameters"][3]["messageText"] - - # Remove any textwrap & TL - jaString = re.sub(r"\n", " ", jaString) - response = translateGPT(jaString, "", False) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Textwrap & Set - translatedText = textwrap.fill(translatedText, width=WIDTH) - codeList[i]["parameters"][3]["messageText"] = translatedText - - ### Choices - jaString = codeList[i]["parameters"][3]["choices"] - matchList = re.findall(r'"label[\\]*":[\\]*"(.*?)[\\]', jaString) - if matchList != None: - # Translate - question = codeList[i]["parameters"][3]["messageText"] - response = translateGPT( - matchList, - f"Previous text for context: {question}\n\nThis will be a dialogue option", - True, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - translatedText = jaString - - # Replace Strings - for j in range(len(matchList)): - translatedText = translatedText.replace(matchList[j], response[0][j]) - - # Set Data - codeList[i]["parameters"][3]["choices"] = translatedText - - if "SoR_GabWindow" in headerString: - argVar = "arg1" - ### Message Text First - if argVar in codeList[i]["parameters"][3]: - jaString = codeList[i]["parameters"][3][argVar] - - # If there isn't any Japanese in the text just skip - if not re.search( - r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", - jaString, - ): - i += 1 - continue - - # Remove any textwrap & TL - jaString = re.sub(r"\n", " ", jaString) - response = translateGPT(jaString, "", False) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Textwrap & Set - translatedText = textwrap.fill(translatedText, width=WIDTH) - codeList[i]["parameters"][3][argVar] = translatedText - pbar.update(1) - - if "TorigoyaMZ_NotifyMessage" in headerString: - argVar = "message" - ### Message Text First - if argVar in codeList[i]["parameters"][3]: - jaString = codeList[i]["parameters"][3][argVar] - - # If there isn't any Japanese in the text just skip - if not re.search( - r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", - jaString, - ): - i += 1 - continue - - # Remove any textwrap & TL - jaString = re.sub(r"\n", " ", jaString) - response = translateGPT(jaString, "", False) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Textwrap & Set - translatedText = textwrap.fill(translatedText, width=WIDTH) - codeList[i]["parameters"][3][argVar] = translatedText - pbar.update(1) - - if "_TMLogWindowMZ" in headerString: - argVar = "text" - ### Message Text First - if argVar in codeList[i]["parameters"][3]: - jaString = codeList[i]["parameters"][3][argVar] - - # If there isn't any Japanese in the text just skip - # if not re.search(r'[一-龠]+|[ぁ-ゔ]+|[ァ-ヴー]+', jaString): - # i += 1 - # continue - - # Remove any textwrap & TL - jaString = re.sub(r"\n", " ", jaString) - response = translateGPT(jaString, "", False) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Textwrap & Set - translatedText = textwrap.fill(translatedText, width=WIDTH) - codeList[i]["parameters"][3][argVar] = translatedText - pbar.update(1) - - if "DestinationWindow" in headerString: - argVar = "destination" - ### Message Text First - if argVar in codeList[i]["parameters"][3]: - jaString = codeList[i]["parameters"][3][argVar] - - # If there isn't any Japanese in the text just skip - # if not re.search(r'[一-龠]+|[ぁ-ゔ]+|[ァ-ヴー]+', jaString): - # i += 1 - # continue - - # Remove any textwrap & TL - jaString = re.sub(r"\n", " ", jaString) - response = translateGPT(jaString, "", False) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Textwrap & Set - translatedText = textwrap.fill(translatedText, width=WIDTH) - codeList[i]["parameters"][3][argVar] = translatedText - pbar.update(1) - - if "MNKR_CommonPopupCoreMZ" in headerString: - argVar = "text" - ### Message Text First - if argVar in codeList[i]["parameters"][3]: - jaString = codeList[i]["parameters"][3][argVar] - - # If there isn't any Japanese in the text just skip - # if not re.search(r'[一-龠]+|[ぁ-ゔ]+|[ァ-ヴー]+', jaString): - # i += 1 - # continue - - # Remove any textwrap & TL - jaString = re.sub(r"\n", " ", jaString) - response = translateGPT(jaString, "", False) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Textwrap & Set - translatedText = textwrap.fill(translatedText, width=WIDTH) - codeList[i]["parameters"][3][argVar] = translatedText - pbar.update(1) - - if "TextPicture" in headerString or "BalloonInBattle" in headerString: - argVar = "text" - font = None - ### Message Text First - if argVar in codeList[i]["parameters"][3]: - acExist = False - jaString = codeList[i]["parameters"][3][argVar] - - # Check ac - if "\\ac" in jaString: - acExist = True - else: - acExist = False - - # If there isn't any Japanese in the text just skip - # if not re.search(r'[一-龠]+|[ぁ-ゔ]+|[ァ-ヴー]+', jaString): - # i += 1 - # continue - - # Remove any textwrap & TL - jaString = re.sub(r"\n", " ", jaString) - if acExist: - jaString = jaString.replace("\\ac ", " ") - jaString = jaString.replace("\\ac", "") - - # Pass 1 - if setData == False: - list357.append(jaString) - - # Pass 2 - else: - if len(list357) > 0: - # Grab and Replace - translatedText = list357[0] - translatedText = jaString.replace(jaString, translatedText) - - # Remove characters that may break scripts - charList = ['"', "\\n"] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Textwrap - translatedText = textwrap.fill(translatedText, WIDTH) - translatedText = translatedText.replace("- ", "-") - - # Center Text - if acExist: - translatedText = f'\\ac {translatedText.replace('\n', '\n\\ac ')}' - - # Check and Set Font - if "fontSize" in codeList[i]["parameters"][3]: - if font: - codeList[i]["parameters"][3]["fontSize"] = font - - # Set - codeList[i]["parameters"][3][argVar] = translatedText - list357.pop(0) - - ## Event Code: 657 [Picture Text] [Optional] - if "code" in codeList[i] and codeList[i]["code"] == 657 and CODE657 is True: - if "text" in codeList[i]["parameters"][0]: - jaString = codeList[i]["parameters"][0] - if not isinstance(jaString, str): - i += 1 - continue - - # Definitely don't want to mess with files - if "_" in jaString: - i += 1 - continue - - # If there isn't any Japanese in the text just skip - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", jaString): - i += 1 - continue - - # Remove outside text - startString = re.search(r"^[^一-龠ぁ-ゔァ-ヴー\<\>【】\\]+", jaString) - jaString = re.sub(r"^[^一-龠ぁ-ゔァ-ヴー\<\>【】\\]+", "", jaString) - endString = re.search(r"[^一-龠ぁ-ゔァ-ヴー\<\>【】。!?\\]+$", jaString) - jaString = re.sub(r"[^一-龠ぁ-ゔァ-ヴー\<\>【】。!?\\]+$", "", jaString) - if startString is None: - startString = "" - else: - startString = startString.group() - if endString is None: - endString = "" - else: - endString = endString.group() - - # Remove any textwrap - jaString = re.sub(r"\n", " ", jaString) - - # Translate - response = translateGPT(jaString, "", True) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - translatedText = response[0] - - # Remove characters that may break scripts - charList = [".", '"', "'"] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Textwrap - translatedText = textwrap.fill(translatedText, width=WIDTH) - translatedText = startString + translatedText + endString - - # Set Data - codeList[i]["parameters"][0] = translatedText - - ## Event Code: 101 [Name] [Optional] - if "code" in codeList[i] and codeList[i]["code"] == 101 and CODE101 is True: - # Check Window Type (Certain games switch between 1st line speakers and none) - if FIRSTLINESPEAKERS: - if codeList[i]["parameters"][2] == 0: - speakerWindow = True - else: - speakerWindow = False - - else: - isVar = False - - # Grab String - jaString = "" - if len(codeList[i]["parameters"]) > 4: - jaString = codeList[i]["parameters"][4] - # Check for Var - elif len(codeList[i]["parameters"]) > 0: - jaString = codeList[i]["parameters"][0] - isVar = True - if not isinstance(jaString, str): - i += 1 - continue - - # Force Speaker using var - if "\\ap[1左]" in jaString.lower() or "\\ap[1右]" in jaString.lower(): - speaker = "Cecily" - i += 1 - continue - elif "\\ap[2左]" in jaString.lower() or "\\ap[2右]" in jaString.lower(): - speaker = "Amelia" - i += 1 - continue - elif "\\ap[3左]" in jaString.lower() or "\\ap[3右]" in jaString.lower(): - speaker = "Henry" - i += 1 - continue - elif "\\ap[4左]" in jaString.lower() or "\\ap[4右]" in jaString.lower(): - speaker = "Oswald" - i += 1 - continue - elif "\\ap" in jaString: - speaker = re.search(r"[\\]+AP\[(.*?)\]", jaString).group(1) - i += 1 - continue - - # Get Speaker - if "\\" not in jaString: - response = getSpeaker(jaString) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - speaker = response[0] - - # Validate Speaker is not empty - if len(speaker) > 0: - if isVar == False: - codeList[i]["parameters"][4] = speaker - i += 1 - continue - else: - codeList[i]["parameters"][0] = speaker - isVar = False - i += 1 - continue - else: - speaker = "" - - ## Event Code: 355 or 655 Scripts [Optional] - if "code" in codeList[i] and (codeList[i]["code"] == 355 or codeList[i]["code"] == 655) and CODE355655 is True: - jaString = codeList[i]["parameters"][0] - regex = r'.*subject=(.*?)"' - - # Var Text - match = re.search(regex, jaString) - if re.search(regex, jaString): - finalJAString = match.group(1) - # Pass 1 - if setData is False: - list355655.append(finalJAString) - - # Pass 2 - else: - # Grab and Replace - translatedText = list355655[0] - translatedText = translatedText.replace("'", "\\'") - - # Set - codeList[i]["parameters"][0] = codeList[i]["parameters"][0].replace(finalJAString, translatedText) - list355655.pop(0) - - ## Event Code: 408 (Script) - if "code" in codeList[i] and (codeList[i]["code"] == 408) and CODE408 is True: - jaString = codeList[i]["parameters"][0] - - # If there isn't any Japanese in the text just skip - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", jaString): - i += 1 - continue - - if "secretText" in jaString: - regex = r"secretText:\s?(.+)" - elif "title" in jaString: - regex = r"title:\s?(.+)" - else: - regex = r"(.+)" - - # Need to remove outside code and put it back later - matchList = re.findall(regex, jaString) - - for match in matchList: - # Remove Textwrap - match = match.replace("\n", " ") - response = translateGPT( - match, - "Reply with the " + LANGUAGE + " translation of the achievement title.", - False, - ) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Replace - translatedText = jaString.replace(match, translatedText) - - # Remove characters that may break scripts - charList = [".", '"', "\\n"] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Textwrap - translatedText = textwrap.fill(translatedText, width=WIDTH) - - # Set Data - codeList[i]["parameters"][0] = translatedText - - ## Event Code: 108 (Script) - if "code" in codeList[i] and (codeList[i]["code"] == 108) and CODE108 is True: - jaString = codeList[i]["parameters"][0] - - # If there isn't any Japanese in the text just skip - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", jaString): - i += 1 - continue - - # Translate - if "info:" in jaString: - regex = r"info:(.*)" - elif "ActiveMessage:" in jaString: - regex = r"" - elif "event_text" in jaString: - regex = r"event_text\s*:\s*(.*)" - else: - i += 1 - continue - - # Need to remove outside code and put it back later - match = re.search(regex, jaString) - if match: - # Pass 1 - if setData is False: - list108.append(match.group(1)) - - # Pass 2 - else: - # Grab and Replace - translatedText = list108[0] - list108.pop(0) - - # Remove characters that may break scripts - charList = [".", '"'] - for char in charList: - translatedText = translatedText.replace(char, "") - translatedText = translatedText.replace('"', '"') - translatedText = translatedText.replace(" ", "_") - translatedText = jaString.replace(match.group(1), translatedText) - - # Set Data - codeList[i]["parameters"][0] = translatedText - - ## Event Code: 356 - if "code" in codeList[i] and codeList[i]["code"] == 356 and CODE356 is True: - jaString = codeList[i]["parameters"][0] - oldjaString = jaString - - # Grab Speaker - if "Tachie showName" in jaString: - matchList = re.findall(r"Tachie showName (.+)", jaString) - if len(matchList) > 0: - # Translate - response = translateGPT( - matchList[0], - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Text - speaker = translatedText - speaker = speaker.replace(" ", " ") - codeList[i]["parameters"][0] = jaString.replace(matchList[0], speaker) - i += 1 - continue - - # Want to translate this script - if "D_TEXT " in jaString: - regex = r"D_TEXT\s(.*?)\s.+" - elif "ShowInfo" in jaString: - regex = r"ShowInfo\s(.*)" - elif "PushGab" in jaString: - regex = r"PushGab\s(.*)" - elif "addLog" in jaString: - regex = r"addLog\s(.*)" - elif "DW_" in jaString: - regex = r"DW_.*?\s(.*)" - elif "CommonPopup" in jaString: - regex = r"CommonPopup\sadd\stext:(.*?)[\\]+}" - else: - regex = r"" - - # Remove any textwrap - jaString = re.sub(r"\n", "_", jaString) - - # Capture Arguments and text - textMatch = re.search(regex, jaString) - if textMatch and textMatch.group(0) != "": - text = textMatch.group(1) - - # Pass 1 - if setData == False: - text = text.replace("_", " ") - list356.append(text) - - # Pass 2 - else: - if len(list356) > 0: - # Grab - translatedText = list356[0] - - # Remove characters that may break scripts - charList = [".", '"'] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Cant have spaces? - translatedText = translatedText.replace(" ", "_") - translatedText = translatedText.replace("__", "_") - - # Put Args Back - translatedText = jaString.replace(text, translatedText) - - # Set Data - codeList[i]["parameters"][0] = translatedText - list356.pop(0) - - if "namePop" in jaString: - matchList = re.findall(r"namePop\s\d+\s(.+?)\s.+", jaString) - if len(matchList) > 0: - # Translate - text = matchList[0] - response = translateGPT(text, "Reply with the " + LANGUAGE + " Translation", False) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Data - translatedText = jaString.replace(text, translatedText) - codeList[i]["parameters"][0] = translatedText - - if "LL_InfoPopupWIndowMV" in jaString: - matchList = re.findall(r"LL_InfoPopupWIndowMV\sshowWindow\s(.+?) .+", jaString) - if len(matchList) > 0: - # Translate - text = matchList[0] - response = translateGPT(text, "Reply with the " + LANGUAGE + " Translation", False) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Data - translatedText = translatedText.replace(" ", "_") - translatedText = jaString.replace(text, translatedText) - codeList[i]["parameters"][0] = translatedText - - if "OriginMenuStatus SetParam" in jaString: - matchList = re.findall(r"OriginMenuStatus\sSetParam\sparam[\d]\s(.*)", jaString) - if len(matchList) > 0: - # Translate - text = matchList[0] - response = translateGPT(text, "Reply with the " + LANGUAGE + " Translation", False) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Data - translatedText = translatedText.replace(" ", "_") - translatedText = jaString.replace(text, translatedText) - codeList[i]["parameters"][0] = translatedText - - # LL_GalgeChoiceWindowMV Message - if "LL_GalgeChoiceWindowMV setMessageText" in jaString: - ### Message Text First - match = re.search(r"LL_GalgeChoiceWindowMV setMessageText (.+)", jaString) - if match: - jaString = match.group(1) - - # Remove any textwrap & TL - jaString = re.sub(r"\n", " ", jaString) - response = translateGPT(jaString, "", False) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Textwrap & Replace Whitespace - translatedText = textwrap.fill(translatedText, width=WIDTH) - translatedText = translatedText.replace(" ", "_") - - # Replace and Set - translatedText = match.group(0).replace(match.group(1), translatedText) - codeList[i]["parameters"][0] = translatedText - - # LL_GalgeChoiceWindowMV Choices - if "LL_GalgeChoiceWindowMV setChoices": - match = re.search(r"LL_GalgeChoiceWindowMV setChoices (.+)", jaString) - if match: - jaString = match.group(1) - choiceList = jaString.split(",") - - # Translate - question = translatedText - response = translateGPT( - choiceList, - f"Previous text for context: {question}\n\nThis will be a dialogue option", - True, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - choiceListTL = response[0] - translatedText = match.group(0) - - # Replace Strings - for j in range(len(choiceListTL)): - choiceListTL[j] = choiceListTL[j].replace(" ", "_") - translatedText = translatedText.replace(choiceList[j], choiceListTL[j]) - - # Set Data - codeList[i]["parameters"][0] = translatedText - - ### Event Code: 102 Show Choice - if "code" in codeList[i] and codeList[i]["code"] == 102 and CODE102 is True: - choiceList = [] - varList = [] - for choice in range(len(codeList[i]["parameters"][0])): - jaString = codeList[i]["parameters"][0][choice] - jaString = jaString.replace(" 。", ".") - - # Avoid Empty Strings - if jaString == "": - i += 1 - continue - - # If and En Statements - ifVar = "" - ifList = re.findall(r"([ei][nf]\(.+?\)\)?\)?)", jaString) - if len(ifList) != 0: - for var in ifList: - jaString = jaString.replace(var, "") - ifVar += var - varList.append(ifVar) - - # Append to List - choiceList.append(jaString) - - # Translate - if len(textHistory) > 0: - response = translateGPT( - choiceList, - "This will be a dialogue option.\nPrevious text for context: " + str(textHistory), - True, - ) - translatedTextList = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - else: - response = translateGPT(choiceList, "This will be a dialogue option", True) - translatedTextList = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Check Mismatch - if len(translatedTextList) == len(choiceList): - for choice in range(len(codeList[i]["parameters"][0])): - translatedText = translatedTextList[choice] - - # Set Data - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if translatedText != "": - translatedText = varList[choice] + translatedText[0].upper() + translatedText[1:] - else: - translatedText = varList[choice] + translatedText - codeList[i]["parameters"][0][choice] = translatedText - else: - if filename not in MISMATCH: - MISMATCH.append(filename) - - ### Event Code: 111 Script - if "code" in codeList[i] and codeList[i]["code"] == 111 and CODE111 is True: - for j in range(len(codeList[i]["parameters"])): - jaString = codeList[i]["parameters"][j] - - # Check if String - if not isinstance(jaString, str): - i += 1 - continue - - # Only TL the Game Variable - if "$gameVariables" not in jaString: - i += 1 - continue - - # This is going to be the var being set. (IMPORTANT) - if "1045" not in jaString: - i += 1 - continue - - # Need to remove outside code and put it back later - matchList = re.findall(r"'(.*?)'", jaString) - - for match in matchList: - response = translateGPT(match, "", False) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Remove characters that may break scripts - charList = [".", '"', "'", "\\n"] - for char in charList: - translatedText = translatedText.replace(char, "") - - jaString = jaString.replace(match, translatedText) - - # Set Data - translatedText = jaString - codeList[i]["parameters"][j] = translatedText - - ### Event Code: 320 Set Variable - if "code" in codeList[i] and codeList[i]["code"] == 320 and CODE320 is True: - jaString = codeList[i]["parameters"][1] - if not isinstance(jaString, str): - i += 1 - continue - - # Definitely don't want to mess with files - if "■" in jaString or "_" in jaString: - i += 1 - continue - - # If there isn't any Japanese in the text just skip - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", jaString): - i += 1 - continue - - # Translate - response = getSpeaker(jaString) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Remove characters that may break scripts - charList = [".", '"', "'", "\\n"] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Set Data - codeList[i]["parameters"][1] = translatedText - - # Iterate - else: - i += 1 - - # EOF - list401TL = [] - list122TL = [] - list356TL = [] - list357TL = [] - list355655TL = [] - list108TL = [] - setData = False - PBAR = pbar - - # 401 - if len(list401) > 0: - response = translateGPT(list401, "", True) - list401TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list401TL) != len(list401): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - else: - setData = True - - # 122 - if len(list122) > 0: - response = translateGPT(list122, "Keep your translation as brief as possible", True) - list122TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list122TL) != len(list122): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - else: - setData = True - - # 355/655 - if len(list355655) > 0: - response = translateGPT(list355655, textHistory, True) - list355655TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list355655TL) != len(list355655): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - else: - setData = True - - # 108 - if len(list108) > 0: - response = translateGPT(list108, textHistory, True) - list108TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list108TL) != len(list108): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - else: - setData = True - - # 356 - if len(list356) > 0: - response = translateGPT(list356, textHistory, True) - list356TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list356TL) != len(list356): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - else: - setData = True - - # 357 - if len(list357) > 0: - response = translateGPT(list357, textHistory, True) - list357TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list357TL) != len(list357): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - else: - setData = True - - # Start Pass 2 - if setData: - searchCodes( - page, - pbar, - [list401TL, list122TL, list355655TL, list108TL, list356TL, list357TL], - filename, - ) - - # Delete all -1 codes - codeListFinal = [] - for i in range(len(codeList)): - if "code" in codeList[i] and codeList[i]["code"] != -1: - codeListFinal.append(codeList[i]) - - # Normal Format - if "list" in page: - page["list"] = codeListFinal - - # Special Format (Scenario) - else: - page = codeListFinal - - except IndexError as e: - traceback.print_exc() - raise Exception(str(e) + "Failed to translate: " + oldjaString) from None - except Exception as e: - traceback.print_exc() - raise Exception(str(e) + "Failed to translate: " + oldjaString) from None - - return totalTokens - - -def searchSS(state, pbar): - totalTokens = [0, 0] - - # Name - nameResponse = ( - translateGPT( - state["name"], - "Reply with only the " + LANGUAGE + " translation of the RPG Skill name.", - False, - ) - if "name" in state - else "" - ) - - # Description - descriptionResponse = ( - translateGPT( - state["description"], - "Reply with only the " + LANGUAGE + " translation of the description.", - False, - ) - if "description" in state - else "" - ) - - # Messages - message1Response = "" - message4Response = "" - message2Response = "" - message3Response = "" - - if "message1" in state: - if len(state["message1"]) > 0 and state["message1"][0] in [ - "は", - "を", - "の", - "に", - "が", - ]: - message1Response = translateGPT( - "Taro" + state["message1"], - "reply with only the gender neutral " - + LANGUAGE - + " translation of the action log. Always start the sentence with Taro. For example,\ -Translate 'Taroを倒した!' as 'Taro was defeated!'", - False, - ) - else: - message1Response = translateGPT( - state["message1"], - "reply with only the gender neutral " + LANGUAGE + " translation", - False, - ) - - if "message2" in state: - if len(state["message2"]) > 0 and state["message2"][0] in [ - "は", - "を", - "の", - "に", - "が", - ]: - message2Response = translateGPT( - "Taro" + state["message2"], - "reply with only the gender neutral " - + LANGUAGE - + " translation of the action log. Always start the sentence with Taro. For example,\ -Translate 'Taroを倒した!' as 'Taro was defeated!'", - False, - ) - else: - message2Response = translateGPT( - state["message2"], - "reply with only the gender neutral " + LANGUAGE + " translation", - False, - ) - - if "message3" in state: - if len(state["message3"]) > 0 and state["message3"][0] in [ - "は", - "を", - "の", - "に", - "が", - ]: - message3Response = translateGPT( - "Taro" + state["message3"], - "reply with only the gender neutral " - + LANGUAGE - + " translation of the action log. Always start the sentence with Taro. For example,\ -Translate 'Taroを倒した!' as 'Taro was defeated!'", - False, - ) - else: - message3Response = translateGPT( - state["message3"], - "reply with only the gender neutral " + LANGUAGE + " translation", - False, - ) - - if "message4" in state: - if len(state["message4"]) > 0 and state["message4"][0] in [ - "は", - "を", - "の", - "に", - "が", - ]: - message4Response = translateGPT( - "Taro" + state["message4"], - "reply with only the gender neutral " - + LANGUAGE - + " translation of the action log. Always start the sentence with Taro. For example,\ -Translate 'Taroを倒した!' as 'Taro was defeated!'", - False, - ) - else: - message4Response = translateGPT( - state["message4"], - "reply with only the gender neutral " + LANGUAGE + " translation", - False, - ) - - # Translate State Notes - if "help" in state["note"]: - noteResponse = translateNote(state, r"]*)>") - totalTokens[0] += noteResponse[0] - totalTokens[1] += noteResponse[1] - if "STATE_HELP" in state["note"]: - noteResponse = translateNote(state, r"\n(.*)\n") - totalTokens[0] += noteResponse[0] - totalTokens[1] += noteResponse[1] - - # Count totalTokens - totalTokens[0] += nameResponse[1][0] if nameResponse != "" else 0 - totalTokens[1] += nameResponse[1][1] if nameResponse != "" else 0 - totalTokens[0] += descriptionResponse[1][0] if descriptionResponse != "" else 0 - totalTokens[1] += descriptionResponse[1][1] if descriptionResponse != "" else 0 - totalTokens[0] += message1Response[1][0] if message1Response != "" else 0 - totalTokens[1] += message1Response[1][1] if message1Response != "" else 0 - totalTokens[0] += message2Response[1][0] if message2Response != "" else 0 - totalTokens[1] += message2Response[1][1] if message2Response != "" else 0 - totalTokens[0] += message3Response[1][0] if message3Response != "" else 0 - totalTokens[1] += message3Response[1][1] if message3Response != "" else 0 - totalTokens[0] += message4Response[1][0] if message4Response != "" else 0 - totalTokens[1] += message4Response[1][1] if message4Response != "" else 0 - - # Set Data - if "name" in state: - state["name"] = nameResponse[0].replace('"', "") - if "description" in state: - # Textwrap - translatedText = descriptionResponse[0] - translatedText = textwrap.fill(translatedText, width=LISTWIDTH) - state["description"] = translatedText.replace('"', "") - if "message1" in state: - state["message1"] = message1Response[0].replace('"', "").replace("Taro", "") - if "message2" in state: - state["message2"] = message2Response[0].replace('"', "").replace("Taro", "") - if "message3" in state: - state["message3"] = message3Response[0].replace('"', "").replace("Taro", "") - if "message4" in state: - state["message4"] = message4Response[0].replace('"', "").replace("Taro", "") - - return totalTokens - - -def searchSystem(data, pbar): - totalTokens = [0, 0] - context = "Reply with only the " + LANGUAGE + ' translation of the UI textbox."' - - # Title - response = translateGPT( - data["gameTitle"], - " Reply with the " + LANGUAGE + " translation of the game title name", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - data["gameTitle"] = response[0].strip(".") - - # Terms - for term in data["terms"]: - if term != "messages": - termList = data["terms"][term] - for i in range(len(termList)): # Last item is a messages object - if termList[i] is not None: - response = translateGPT(termList[i], context, False) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - termList[i] = response[0].replace('"', "").strip() - - # Armor Types - for i in range(len(data["armorTypes"])): - response = translateGPT( - data["armorTypes"][i], - "Reply with only the " + LANGUAGE + " translation of the armor type", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - data["armorTypes"][i] = response[0].replace('"', "").strip() - - # Skill Types - for i in range(len(data["skillTypes"])): - response = translateGPT( - data["skillTypes"][i], - "Reply with only the " + LANGUAGE + " translation", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - data["skillTypes"][i] = response[0].replace('"', "").strip() - - # Equip Types - for i in range(len(data["equipTypes"])): - response = translateGPT( - data["equipTypes"][i], - "Reply with only the " + LANGUAGE + " translation of the equipment type. No disclaimers.", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - data["equipTypes"][i] = response[0].replace('"', "").strip() - - # # Variables (Optional ususally) - # for i in range(len(data['variables'])): - # response = translateGPT(data['variables'][i], 'Reply with only the '+ LANGUAGE +' translation of the title', False) - # totalTokens[0] += response[1][0] - # totalTokens[1] += response[1][1] - # data['variables'][i] = response[0].replace('\"', '').strip() - - # Messages - messages = data["terms"]["messages"] - for key, value in messages.items(): - response = translateGPT( - value, - "Reply with only the " - + LANGUAGE - + ' translation of the battle text.\nTranslate "常時ダッシュ" as "Always Dash"\nTranslate "次の%1まで" as Next %1.', - False, - ) - translatedText = response[0] - - # Remove characters that may break scripts - charList = [".", '"', "\\n"] - for char in charList: - translatedText = translatedText.replace(char, "") - - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - messages[key] = translatedText - - return totalTokens - - -# Save some money and enter the character before translation -def getSpeaker(speaker): - match speaker: - case "ファイン": - return ["Fine", [0, 0]] - case "": - return ["", [0, 0]] - case _: - # Find Speaker - for i in range(len(NAMESLIST)): - if speaker == NAMESLIST[i][0]: - return [NAMESLIST[i][1], [0, 0]] - - # Translate and Store Speaker - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - True, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - response[0] = response[0].replace("Speaker: ", "") - - # Retry if name doesn't translate for some reason - if re.search(r"([a-zA-Z??])", response[0]) == None: - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - - speakerList = [speaker, response[0]] - NAMESLIST.append(speakerList) - return response - return [speaker, [0, 0]] - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT, format): - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - if format == "json": - user = f"```json\n{subbedT}\n```" - else: - user = subbedT - return system, user - - -def translateText(system, user, history, penalty, format, model=MODEL): - # Prompt - msg = [{"role": "system", "content": system}] - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Response Format - if format == "json": - responseFormat = {"type": "json_object"} - else: - responseFormat = {"type": "text"} - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=model, - response_format=responseFormat, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "「": '\\"', - "」": '\\"', - "- ": "-", - "—": "―", - "】": "]", - "【": "[", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Remove Repeating Characters - pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") - translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - try: - translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) - translatedTextList = re.sub(r"(? instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) +PBAR = None +FILENAME = None + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 + FREQUENCY_PENALTY = 0.2 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 30 + FREQUENCY_PENALTY = 0.1 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False + +# Dialogue / Scroll / Choices (Main Codes) +CODE401 = False +CODE405 = False +CODE102 = False + +# Optional +CODE101 = False # Turn this one when names exist in 101 +CODE408 = False # Warning, translates comments and can inflate costs. + +# Variables +CODE122 = False + +# Other +CODE355655 = False +CODE357 = False +CODE657 = False +CODE356 = False +CODE320 = False +CODE324 = False +CODE111 = False +CODE108 = True + + +def handleMVMZ(filename, estimate): + global ESTIMATE, TOKENS, FILENAME + ESTIMATE = estimate + FILENAME = filename + + # Translate + start = time.time() + translatedData = openFiles(filename) + + # Translate + if not estimate: + try: + with open("translated/" + filename, "w", encoding="utf-8") as outFile: + json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4) + except Exception: + traceback.print_exc() + return "Fail" + + # Print File + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="utf-8-sig") as f: + data = json.load(f) + + # Map Files + if "Map" in filename and filename != "MapInfos.json": + translatedData = parseMap(data, filename) + + # CommonEvents Files + elif "CommonEvents" in filename: + translatedData = parseCommonEvents(data, filename) + + # Actor File + elif "Actors" in filename: + translatedData = parseNames(data, filename, "Actors") + + # Armor File + elif "Armors" in filename: + translatedData = parseNames(data, filename, "Armors") + + # Weapons File + elif "Weapons" in filename: + translatedData = parseNames(data, filename, "Weapons") + + # Classes File + elif "Classes" in filename: + translatedData = parseNames(data, filename, "Classes") + + # Enemies File + elif "Enemies" in filename: + translatedData = parseNames(data, filename, "Enemies") + + # Items File + elif "Items" in filename: + translatedData = parseNames(data, filename, "Items") + + # MapInfo File + elif "MapInfos" in filename: + translatedData = parseNames(data, filename, "MapInfos") + + # Skills File + elif "Skills" in filename: + translatedData = parseNames(data, filename, "Skills") + + # Troops File + elif "Troops" in filename: + translatedData = parseTroops(data, filename) + + # States File + elif "States" in filename: + translatedData = parseSS(data, filename) + + # System File + elif "System" in filename: + translatedData = parseSystem(data, filename) + + # Scenario File + elif "Scenario" in filename: + translatedData = parseScenario(data, filename) + + else: + raise NameError(filename + " Not Supported") + + return translatedData + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] is None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def parseMap(data, filename): + totalTokens = [0, 0] + totalLines = 0 + events = data["events"] + global LOCK + + # Translate displayName for Map files + if "Map" in filename: + response = translateGPT( + data["displayName"], + "Reply with only the " + LANGUAGE + " translation of the RPG location name", + False, + ) + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + data["displayName"] = response[0].replace('"', "") + + # Get total for progress bar + for event in events: + if event is not None: + for page in event["pages"]: + totalLines += len(page["list"]) + + # Thread for each page in file + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + with ThreadPoolExecutor(max_workers=THREADS) as executor: + for event in events: + if event is not None: + # This translates ID of events. (May break the game) + if ".*") + totalTokens[0] += response[0] + totalTokens[1] += response[1] + if ".*") + totalTokens[0] += response[0] + totalTokens[1] += response[1] + if ".*") + totalTokens[0] += response[0] + totalTokens[1] += response[1] + + futures = [executor.submit(searchCodes, page, pbar, [], filename) for page in event["pages"] if page is not None] + for future in as_completed(futures): + try: + totalTokensFuture = future.result() + totalTokens[0] += totalTokensFuture[0] + totalTokens[1] += totalTokensFuture[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateNote(event, regex): + # Regex String + jaString = event["note"] + match = re.findall(regex, jaString, re.DOTALL) + if match: + tokens = [0, 0] + i = 0 + while i < len(match): + initialJAString = match[i] + # Remove any textwrap + modifiedJAString = initialJAString.replace("\n", " ") + + # Translate + response = translateGPT( + modifiedJAString, + "Reply with only the " + LANGUAGE + " translation.", + False, + ) + translatedText = response[0] + tokens[0] += response[1][0] + tokens[1] += response[1][1] + + # Textwrap + translatedText = textwrap.fill(translatedText, width=NOTEWIDTH) + translatedText = translatedText.replace('"', "") + jaString = jaString.replace(initialJAString, translatedText) + event["note"] = jaString + i += 1 + return tokens + return [0, 0] + + +# For notes that can't have spaces. +def translateNoteOmitSpace(event, regex): + # Regex that only matches text inside LB. + jaString = event["note"] + + match = re.findall(regex, jaString, re.DOTALL) + if match: + oldJAString = match[0] + # Remove any textwrap + jaString = re.sub(r"\n", " ", oldJAString) + + # Translate + response = translateGPT( + jaString, + "Reply with the " + LANGUAGE + " translation of the location name.", + False, + ) + translatedText = response[0] + + translatedText = translatedText.replace('"', "") + translatedText = translatedText.replace(" ", "_") + event["note"] = event["note"].replace(oldJAString, translatedText) + return response[1] + return [0, 0] + + +def parseCommonEvents(data, filename): + totalTokens = [0, 0] + totalLines = 0 + global LOCK + + # Get total for progress bar + for page in data: + if page is not None: + totalLines += len(page["list"]) + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + with ThreadPoolExecutor(max_workers=THREADS) as executor: + futures = [executor.submit(searchCodes, page, pbar, [], filename) for page in data if page is not None] + for future in as_completed(futures): + try: + totalTokensFuture = future.result() + totalTokens[0] += totalTokensFuture[0] + totalTokens[1] += totalTokensFuture[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def parseTroops(data, filename): + totalTokens = [0, 0] + totalLines = 0 + global LOCK + + # Get total for progress bar + for troop in data: + if troop is not None: + for page in troop["pages"]: + totalLines += len(page["list"]) + 1 # The +1 is because each page has a name. + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + for troop in data: + if troop is not None: + with ThreadPoolExecutor(max_workers=THREADS) as executor: + futures = [executor.submit(searchCodes, page, pbar, [], filename) for page in troop["pages"] if page is not None] + for future in as_completed(futures): + try: + totalTokensFuture = future.result() + totalTokens[0] += totalTokensFuture[0] + totalTokens[1] += totalTokensFuture[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def parseNames(data, filename, context): + totalTokens = [0, 0] + totalLines = 0 + totalLines += len(data) + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + try: + result = searchNames(data, pbar, context) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def parseSS(data, filename): + totalTokens = [0, 0] + totalLines = 0 + totalLines += len(data) + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + for ss in data: + if ss is not None: + try: + result = searchSS(ss, pbar) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def parseSystem(data, filename): + totalTokens = [0, 0] + totalLines = 0 + + # Calculate Total Lines + for term in data["terms"]: + termList = data["terms"][term] + totalLines += len(termList) + totalLines += len(data["gameTitle"]) + totalLines += len(data["terms"]["messages"]) + totalLines += len(data["variables"]) + totalLines += len(data["equipTypes"]) + totalLines += len(data["armorTypes"]) + totalLines += len(data["skillTypes"]) + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + try: + result = searchSystem(data, pbar) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def parseScenario(data, filename): + totalTokens = [0, 0] + totalLines = 0 + global LOCK + + # Get total for progress bar + for page in data.items(): + totalLines += len(page[1]) + + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + with ThreadPoolExecutor(max_workers=THREADS) as executor: + futures = [executor.submit(searchCodes, page[1], pbar, [], filename) for page in data.items() if page[1] is not None] + for future in as_completed(futures): + try: + totalTokensFuture = future.result() + totalTokens[0] += totalTokensFuture[0] + totalTokens[1] += totalTokensFuture[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def searchNames(data, pbar, context): + totalTokens = [0, 0] + nameList = [] + profileList = [] + nicknameList = [] + descriptionList = [] + noteList = [] + i = 0 # Counter + j = 0 # Counter 2 + filling = False + mismatch = False + batchFull = False + + # Set the context of what we are translating + if "Actors" in context: + newContext = "Reply with only the " + LANGUAGE + " translation of the NPC name" + if "Armors" in context: + newContext = "Reply with only the " + LANGUAGE + " translation of the RPG equipment name" + if "Classes" in context: + newContext = "Reply with only the " + LANGUAGE + " translation of the RPG class name" + if "MapInfos" in context: + newContext = "Reply with only the " + LANGUAGE + " translation of the location name" + if "Enemies" in context: + newContext = "Reply with only the " + LANGUAGE + " translation of the enemy NPC name" + if "Weapons" in context: + newContext = "Reply with only the " + LANGUAGE + " translation of the RPG weapon name" + if "Items" in context: + newContext = "Reply with only the " + LANGUAGE + " translation of the RPG item name" + if "Skills" in context: + newContext = "Reply with only the " + LANGUAGE + " translation of the RPG skill name" + + # Names + with open("translations.txt", "a", encoding="utf-8") as file: + file.write(f"\n#{context}\n") + while i < len(data) or filling == True: + if i < len(data): + # Empty Data + if data[i] is None or data[i]["name"] == "": + i += 1 + + continue + + # Filling up Batch + filling = True + if context in "Actors": + if len(nameList) < BATCHSIZE: + if data[i]["name"] != "": + nameList.append(data[i]["name"]) + if data[i]["nickname"] != "": + nicknameList.append(data[i]["nickname"]) + if data[i]["profile"] != "": + profileList.append(data[i]["profile"].replace("\n", " ")) + + # Notes + if "") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + if "PE拡張" in data[i]["note"]: + tokensResponse = translateNote(data[i], r"") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + i += 1 + else: + batchFull = True + if context in ["Armors", "Weapons", "Items"]: + if len(nameList) < BATCHSIZE: + nameList.append(data[i]["name"]) + if "description" in data[i] and data[i]["description"] != "": + descriptionList.append(data[i]["description"].replace("\n", " ")) + if "") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + if "") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + if "") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + if "") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + if "") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + if "") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + if "") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + if "Switch Shop Description" in data[i]["note"]: + tokensResponse = translateNote(data[i], r"\n(.*)\n") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + if "") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + + i += 1 + else: + batchFull = True + if context in ["Skills"]: + if len(nameList) < BATCHSIZE: + nameList.append(data[i]["name"]) + if "description" in data[i] and data[i]["description"]: + descriptionList.append(data[i]["description"].replace("\n", " ")) + + # Messages + number = 1 + while number < 5: + if f"message{number}" in data[i] and data[i][f"message{number}"]: + if data[i][f"message{number}"][0] in ["は", "を", "の", "に", "が"]: + msgResponse = translateGPT( + "Taro" + data[i][f"message{number}"], + "reply with only the gender neutral " + + LANGUAGE + + " translation of the action log. Always start the sentence with Taro. For example, Translate 'Taroを倒した!' as 'Taro was defeated!'", + False, + ) + data[i][f"message{number}"] = msgResponse[0].replace("Taro", "") + totalTokens[0] += msgResponse[1][0] + totalTokens[1] += msgResponse[1][1] + number += 1 + + else: + msgResponse = translateGPT( + data[i][f"message{number}"], + "reply with only the gender neutral " + LANGUAGE + " translation", + False, + ) + data[i][f"message{number}"] = msgResponse[0] + totalTokens[0] += msgResponse[1][0] + totalTokens[1] += msgResponse[1][1] + number += 1 + else: + number += 1 + + i += 1 + else: + batchFull = True + if context in ["Enemies", "Classes", "MapInfos"]: + if len(nameList) < BATCHSIZE: + nameList.append(data[i]["name"]) + + # Notes + if "note" in data[i]: + if "") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + if "") + totalTokens[0] += tokensResponse[0] + totalTokens[1] += tokensResponse[1] + i += 1 + else: + batchFull = True + + # Batch Full + if batchFull == True or i >= len(data): + k = j # Original Index + if context in "Actors": + # Name + response = translateGPT(nameList, newContext, True) + translatedNameBatch = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Nickname + if nicknameList: + response = translateGPT(nicknameList, newContext, True) + translatedNicknameBatch = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Profile + if profileList: + response = translateGPT(profileList, "", True) + translatedProfileBatch = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Set Data + if len(nameList) == len(translatedNameBatch): + j = k + while j < i: + # Empty Data + if data[j] is None or data[j]["name"] == "": + j += 1 + continue + else: + # Get Text + if data[j]["name"] != "": + with open("translations.txt", "a", encoding="utf-8") as file: + file.write(f'{data[j]["name"]} ({translatedNameBatch[0]})\n') + data[j]["name"] = translatedNameBatch[0] + translatedNameBatch.pop(0) + if data[j]["nickname"] != "": + data[j]["nickname"] = translatedNicknameBatch[0] + translatedNicknameBatch.pop(0) + if data[j]["profile"] != "": + data[j]["profile"] = textwrap.fill(translatedProfileBatch[0], LISTWIDTH) + translatedProfileBatch.pop(0) + + # If Batch is empty. Move on. + if len(translatedNameBatch) == 0: + nameList.clear() + filling = False + j += 1 + else: + mismatch = True + + if context in ["Armors", "Weapons", "Items", "Skills"]: + # Name + response = translateGPT(nameList, newContext, True) + translatedNameBatch = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Description + if descriptionList: + response = translateGPT( + descriptionList, + f"Reply with only the {LANGUAGE} translation of the text.", + True, + ) + translatedDescriptionBatch = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Set Data + if len(nameList) == len(translatedNameBatch): + j = k + with open("translations.txt", "a", encoding="utf-8") as file: + while j < i: + # Empty Data + if data[j] is None or data[j]["name"] == "": + j += 1 + continue + else: + # Get Text + file.write(f'{data[j]['name']} ({translatedNameBatch[0]})\n') + data[j]["name"] = translatedNameBatch[0] + translatedNameBatch.pop(0) + if "description" in data[j] and data[j]["description"] != "": + data[j]["description"] = textwrap.fill(translatedDescriptionBatch[0], LISTWIDTH) + translatedDescriptionBatch.pop(0) + + # If Batch is empty. Move on. + if len(translatedNameBatch) == 0: + nameList.clear() + descriptionList.clear() + batchFull = False + filling = False + j += 1 + else: + mismatch = True + if context in ["Enemies", "Classes", "MapInfos"]: + response = translateGPT(nameList, newContext, True) + translatedNameBatch = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Set Data + if len(nameList) == len(translatedNameBatch): + j = k + while j < i: + # Empty Data + if data[j] is None or data[j]["name"] == "": + j += 1 + continue + else: + with open("translations.txt", "a", encoding="utf-8") as file: + file.write(f'{data[j]["name"]} ({translatedNameBatch[0]})\n') + # Get Text + data[j]["name"] = translatedNameBatch[0] + translatedNameBatch.pop(0) + + # If Batch is empty. Move on. + if len(translatedNameBatch) == 0: + nameList.clear() + batchFull = False + filling = False + j += 1 + else: + mismatch = True + + # Mismatch + if mismatch == True: + MISMATCH.append(nameList) + nameList.clear() + profileList.clear() + descriptionList.clear() + filling = False + mismatch = False + + i += 1 + + return totalTokens + + +def searchCodes(page, pbar, jobList, filename): + if len(jobList) > 0: + list401 = jobList[0] + list122 = jobList[1] + list355655 = jobList[2] + list108 = jobList[3] + list356 = jobList[4] + list357 = jobList[5] + setData = True + else: + list401 = [] + list122 = [] + list355655 = [] + list108 = [] + list356 = [] + list357 = [] + setData = False + textHistory = [] + match = [] + totalTokens = [0, 0] + translatedText = "" + speaker = "" + speakerID = None + syncIndex = 0 + CLFlag = False + maxHistory = MAXHISTORY + VNameValue = None + speakerWindow = FIRSTLINESPEAKERS + global LOCK + global NAMESLIST + global MISMATCH + global PBAR + with LOCK: + PBAR = pbar + + # Begin Parsing File + try: + # Normal Format + if "list" in page: + codeList = page["list"] + + # Special Format (Scenario) + else: + codeList = page + + # Iterate through page + i = 0 + while i < len(codeList): + with LOCK: + # syncIndex will keep i in sync when it gets modified + if syncIndex > i: + i = syncIndex + if len(codeList) <= i: + break + + # Declare Varss + currentGroup = [] + nametag = "" + + ## Event Code: 401 Show Text + if "code" in codeList[i] and codeList[i]["code"] in [401, 405, -1] and (CODE401 or CODE405): + # Save Code and starting index (j) + code = codeList[i]["code"] + j = i + endtag = "" + + # Grab String + if len(codeList[i]["parameters"]) > 0: + jaString = codeList[i]["parameters"][0] + oldjaString = jaString + else: + codeList[i]["code"] = -1 + i += 1 + continue + + # Validate Japanese Text + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", jaString) and IGNORETLTEXT: + i += 1 + continue + + # # For Retarded Devs + # retardRegex = r'([\\]+[nN]\[[\\]+V\[\d*?\]\])' + # match = re.search(retardRegex, jaString) + # if match: + # if VNameValue == 1: + # jaString = re.sub(retardRegex, 'リッカ', jaString) + # if VNameValue == 2: + # jaString = re.sub(retardRegex, 'ミミ', jaString) + # if VNameValue == 3: + # jaString = re.sub(retardRegex, 'ヒトミ', jaString) + # if VNameValue == 4: + # jaString = re.sub(retardRegex, 'Taro', jaString) + # if VNameValue == 5: + # jaString = re.sub(retardRegex, '富士見', jaString) + + # Speaker Check + speakerList = [] + + # m and z Codes + match = re.search(r"(.*?)[\\]+m\[\d+?\][\\]+z\[\d+?\]", jaString) + if match: + speakerList.append(match.group(1)) + if "\\c" in speakerList[0]: + speakerList = re.findall( + r"^[\\]+[cC]\[[\d]+\]【?(.+?)】?[\\]+[Cc]\[[\d]\]\\?\\?$", + speakerList[0], + ) + + # Brackets + if len(speakerList) == 0: + speakerList = re.findall(r"^【(.*?)】$", jaString) + + # Colors + if len(speakerList) == 0: + speakerList = re.findall( + r"^[\\]+[cC]\[[\d]+\]【?(.+?)】?[\\]+[Cc]\[[\d]\]\\?\\?$", + jaString, + ) + + # First Line Speakers + if len(speakerList) == 0 and FIRSTLINESPEAKERS is True: + # Remove any RPGMaker Code at start + ffMatch = re.search( + r"^(\s*[\\]+[aAbBdDeEfFgGhHjJlLmMoOpPqQrRsStTuUwWxXyYzZ]+\[[\w\d\[\]\\]+\])", + jaString, + ) + if ffMatch != None: + jaString = jaString.replace(ffMatch.group(0), "") + nametag += ffMatch.group(0) + + # Test Speaker + if ( + len(jaString) < 40 + and "code" in codeList[i + 1] + and codeList[i + 1]["code"] in [401, 405, -1] + and len(codeList[i + 1]["parameters"]) > 0 + and len(codeList[i + 1]["parameters"][0]) > 0 + ): + if codeList[i + 1]["parameters"] != "" and codeList[i + 1]["parameters"][0].strip()[0] in [ + "「", + '"', + "(", + "(", + "*", + "[", + ]: + speakerList = re.findall(r".+", jaString) + + if len(speakerList) != 0 and codeList[i + 1]["code"] in [401, 405, -1]: + # Get Speaker + response = getSpeaker(speakerList[0]) + speaker = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Set Data + codeList[i]["parameters"][0] = nametag + jaString.replace(speakerList[0], speaker) + nametag = "" + + # Iterate to next string + i += 1 + j = i + while codeList[i]["code"] in [-1]: + i += 1 + j = i + jaString = codeList[i]["parameters"][0] + + # Using this to keep track of 401's in a row. + currentGroup.append(jaString) + + # Join Up 401's into single string + if len(codeList) > i + 1: + while codeList[i + 1]["code"] in [401, 405, -1]: + if setData == True: + codeList[i]["parameters"] = [] + codeList[i]["code"] = -1 + i += 1 + j = i + + # Only add if not empty + if len(codeList[i]["parameters"]) > 0: + jaString = codeList[i]["parameters"][0] + currentGroup.append(jaString) + + # Make sure not the end of the list. + if len(codeList) <= i + 1: + break + + # Format String + if len(currentGroup) > 0: + finalJAString = "\n".join(currentGroup) + oldjaString = finalJAString + + # Check if Empty + if finalJAString == "": + i += 1 + continue + + # Set Back + if setData == True: + codeList[i]["parameters"] = [finalJAString] + + ### \\n + nCase = None + regex = r"([\\]+[kKnN][wWcCrRrEe]?[\[<](.*?)[>\]])" + match = re.search(regex, finalJAString) + + # Set Name + if match: + nametag = match.group(1) + speaker = match.group(2) + + # Translate Speaker + response = getSpeaker(speaker) + tledSpeaker = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Set Nametag and Remove from Final String + finalJAString = finalJAString.replace(nametag, "") + nametag = nametag.replace(speaker, tledSpeaker) + speaker = tledSpeaker + + # Remove Extra Stuff bad for translation. + finalJAString = finalJAString.replace("゙", "") + finalJAString = finalJAString.replace("―", "-") + finalJAString = finalJAString.replace("…", "...") + finalJAString = finalJAString.replace("。", ".") + finalJAString = re.sub(r"(\.{3}\.+)", "...", finalJAString) + finalJAString = finalJAString.replace(" ", "") + finalJAString = finalJAString.replace("「", '"') + finalJAString = finalJAString.replace("」", '"') + + ### Remove format codes + # Furigana + rcodeMatch = re.findall(r"([\\]+[r][b]?\[.*?,(.*?)\])", finalJAString) + if len(rcodeMatch) > 0: + for match in rcodeMatch: + finalJAString = finalJAString.replace(match[0], match[1]) + + # Formatting + formatMatch = re.findall(r"[\\]+[!><.|#^{}]", finalJAString) + if len(formatMatch) > 0: + for match in formatMatch: + finalJAString = finalJAString.replace(match, "") + + # Remove any RPGMaker Code at start + ffMatch = re.search( + r"^(\s*[\\]+[aAbBdDeEfFgGhHjJlLmMoOpPqQrRsStTuUwWxXyYzZ]+\[[\w\d\[\]\\]+?\])", + finalJAString, + ) + if ffMatch != None: + finalJAString = finalJAString.replace(ffMatch.group(1), "") + nametag += ffMatch.group(1) + + # Remove _ABL Codes + ffMatch = re.search(r"^(_ABL).*", finalJAString) + if ffMatch != None: + finalJAString = finalJAString.replace(ffMatch.group(1), "") + nametag += ffMatch.group(1) + + # Center Lines + if "\\CL" in finalJAString or "\\ac" in finalJAString: + finalJAString = finalJAString.replace("\\CL ", "") + finalJAString = finalJAString.replace("\\CL", "") + finalJAString = finalJAString.replace("\\ac ", "") + finalJAString = finalJAString.replace("\\ac", "") + CLFlag = True + + # 1st Passthrough (Grabbing Data) + if setData == False: + # Remove Textwrap + if FIXTEXTWRAP: + finalJAString = finalJAString.replace("\n", " ") + if "\\px[200]" in finalJAString: + finalJAString = finalJAString.replace("\\px[200]", "") + + # Append + if finalJAString != "": + if speaker == "" and finalJAString != "": + list401.append(finalJAString) + elif finalJAString != "": + list401.append(f"[{speaker}]: {finalJAString}") + else: + list401.append(speaker) + speaker = "" + match = [] + nametag = "" + currentGroup = [] + syncIndex = i + 1 + + # Keep textHistory list at length maxHistory + textHistory.append('"' + finalJAString + '"') + if len(textHistory) > maxHistory: + textHistory.pop(0) + + # 2nd Passthrough (Setting Data) + else: + # Grab Translated String + if len(list401) > 0: + translatedText = list401[0] + + # Remove speaker + if speaker != "": + matchSpeakerList = re.findall(r"^\[?(.+?)\]?\s?[|:]\s?", translatedText) + if len(matchSpeakerList) > 0: + newSpeaker = matchSpeakerList[0] + nametag = nametag.replace(speaker, newSpeaker) + translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText) + + # Fix '- ' + translatedText = translatedText.replace("- ", "-") + + # Textwrap + if FIXTEXTWRAP is True: + finalJAString = re.sub(r"\n", " ", finalJAString) + finalJAString = finalJAString.replace("
", " ") + + if FIXTEXTWRAP is True and "_ABL" in nametag: + translatedText = textwrap.fill(translatedText, width=100) + elif FIXTEXTWRAP is True: + translatedText = textwrap.fill(translatedText, width=WIDTH) + + # BR Flag + if BRFLAG is True: + translatedText = translatedText.replace("\n", "
") + + # px + if "\\px[200]" in nametag: + translatedText = translatedText.replace("\\px[200]", "") + translatedText = translatedText.replace("\n", "\n\\px[200]") + + ### Add Var Strings + # CL Flag + if CLFlag: + translatedText = "\\ac " + translatedText + translatedText = translatedText.replace("\n", "\n\\ac ") + translatedText = re.sub(r"[\\]+?ac\s+", r"\\ac ", translatedText) + CLFlag = False + + # Nametag + if nCase == 0: + translatedText = translatedText + nametag + else: + translatedText = nametag + translatedText + nametag = "" + + # Endtag + if endtag != "": + translatedText = translatedText + endtag + endtag = "" + + # Set Data + codeList[j]["parameters"] = [translatedText] + codeList[j]["code"] = code + speaker = "" + match = [] + currentGroup = [] + syncIndex = i + 1 + list401.pop(0) + + ## Event Code: 122 [Set Variables] + if "code" in codeList[i] and codeList[i]["code"] == 122 and CODE122 is True: + # This is going to be the var being set. (IMPORTANT) + if codeList[i]["parameters"][0] not in list(range(42, 45)): + i += 1 + continue + + jaString = codeList[i]["parameters"][4] + + # # For Retarded Devs + # VNameValue = jaString + # i += 1 + # continue + + # Definitely don't want to mess with files + # if 'gameV' in jaString or '_' in jaString: + # i += 1 + # continue + + # Validate String + if not isinstance(jaString, str): + i += 1 + continue + + # Set String + matchedText = None + if len(re.findall(r"([\'\"\`])", jaString)) >= 2: + matchedText = re.search(r"[\'\"\`](.*)[\'\"\`]", jaString) + # else: + # matchedText = re.search(r'(.*)', jaString) + + # Last Check + if matchedText.group(1) != None and matchedText.group(1) != " ": + # Remove Textwrap + finalJAString = matchedText.group(1).replace("\\n", " ") + + # Pass 1 + if setData == False: + if finalJAString != "": + list122.append(finalJAString) + + # Pass 2 + else: + if len(list122) > 0: + # Grab and Replace + translatedText = list122[0] + translatedText = jaString.replace(jaString, translatedText) + + # Remove characters that may break scripts + charList = ['"', "\\n"] + for char in charList: + translatedText = translatedText.replace(char, "") + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + translatedText = translatedText.replace("\n", "\\n") + + # Set + codeList[i]["parameters"][4] = f"`{translatedText}`" + list122.pop(0) + + ## Event Code: 357 [Picture Text] [Optional] + if "code" in codeList[i] and codeList[i]["code"] == 357 and CODE357 is True: + headerString = codeList[i]["parameters"][0] + + if headerString == "LL_GalgeChoiceWindow": + ### Message Text First + jaString = codeList[i]["parameters"][3]["messageText"] + + # Remove any textwrap & TL + jaString = re.sub(r"\n", " ", jaString) + response = translateGPT(jaString, "", False) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Textwrap & Set + translatedText = textwrap.fill(translatedText, width=WIDTH) + codeList[i]["parameters"][3]["messageText"] = translatedText + + ### Choices + jaString = codeList[i]["parameters"][3]["choices"] + matchList = re.findall(r'"label[\\]*":[\\]*"(.*?)[\\]', jaString) + if matchList != None: + # Translate + question = codeList[i]["parameters"][3]["messageText"] + response = translateGPT( + matchList, + f"Previous text for context: {question}\n", + True, + ) + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + translatedText = jaString + + # Replace Strings + for j in range(len(matchList)): + translatedText = translatedText.replace(matchList[j], response[0][j]) + + # Set Data + codeList[i]["parameters"][3]["choices"] = translatedText + + if "SoR_GabWindow" in headerString: + argVar = "arg1" + ### Message Text First + if argVar in codeList[i]["parameters"][3]: + jaString = codeList[i]["parameters"][3][argVar] + + # If there isn't any Japanese in the text just skip + if not re.search( + r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", + jaString, + ): + i += 1 + continue + + # Remove any textwrap & TL + jaString = re.sub(r"\n", " ", jaString) + response = translateGPT(jaString, "", False) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Textwrap & Set + translatedText = textwrap.fill(translatedText, width=WIDTH) + codeList[i]["parameters"][3][argVar] = translatedText + pbar.update(1) + + if "TorigoyaMZ_NotifyMessage" in headerString: + argVar = "message" + ### Message Text First + if argVar in codeList[i]["parameters"][3]: + jaString = codeList[i]["parameters"][3][argVar] + + # If there isn't any Japanese in the text just skip + if not re.search( + r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", + jaString, + ): + i += 1 + continue + + # Remove any textwrap & TL + jaString = re.sub(r"\n", " ", jaString) + response = translateGPT(jaString, "", False) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Textwrap & Set + translatedText = textwrap.fill(translatedText, width=WIDTH) + codeList[i]["parameters"][3][argVar] = translatedText + pbar.update(1) + + if "_TMLogWindowMZ" in headerString: + argVar = "text" + ### Message Text First + if argVar in codeList[i]["parameters"][3]: + jaString = codeList[i]["parameters"][3][argVar] + + # If there isn't any Japanese in the text just skip + # if not re.search(r'[一-龠]+|[ぁ-ゔ]+|[ァ-ヴー]+', jaString): + # i += 1 + # continue + + # Remove any textwrap & TL + jaString = re.sub(r"\n", " ", jaString) + response = translateGPT(jaString, "", False) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Textwrap & Set + translatedText = textwrap.fill(translatedText, width=WIDTH) + codeList[i]["parameters"][3][argVar] = translatedText + pbar.update(1) + + if "DestinationWindow" in headerString: + argVar = "destination" + ### Message Text First + if argVar in codeList[i]["parameters"][3]: + jaString = codeList[i]["parameters"][3][argVar] + + # If there isn't any Japanese in the text just skip + # if not re.search(r'[一-龠]+|[ぁ-ゔ]+|[ァ-ヴー]+', jaString): + # i += 1 + # continue + + # Remove any textwrap & TL + jaString = re.sub(r"\n", " ", jaString) + response = translateGPT(jaString, "", False) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Textwrap & Set + translatedText = textwrap.fill(translatedText, width=WIDTH) + codeList[i]["parameters"][3][argVar] = translatedText + pbar.update(1) + + if "MNKR_CommonPopupCoreMZ" in headerString: + argVar = "text" + ### Message Text First + if argVar in codeList[i]["parameters"][3]: + jaString = codeList[i]["parameters"][3][argVar] + + # If there isn't any Japanese in the text just skip + # if not re.search(r'[一-龠]+|[ぁ-ゔ]+|[ァ-ヴー]+', jaString): + # i += 1 + # continue + + # Remove any textwrap & TL + jaString = re.sub(r"\n", " ", jaString) + response = translateGPT(jaString, "", False) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Textwrap & Set + translatedText = textwrap.fill(translatedText, width=WIDTH) + codeList[i]["parameters"][3][argVar] = translatedText + pbar.update(1) + + if "TextPicture" in headerString or "BalloonInBattle" in headerString: + argVar = "text" + font = None + ### Message Text First + if argVar in codeList[i]["parameters"][3]: + acExist = False + jaString = codeList[i]["parameters"][3][argVar] + + # Check ac + if "\\ac" in jaString: + acExist = True + else: + acExist = False + + # If there isn't any Japanese in the text just skip + # if not re.search(r'[一-龠]+|[ぁ-ゔ]+|[ァ-ヴー]+', jaString): + # i += 1 + # continue + + # Remove any textwrap & TL + jaString = re.sub(r"\n", " ", jaString) + if acExist: + jaString = jaString.replace("\\ac ", " ") + jaString = jaString.replace("\\ac", "") + + # Pass 1 + if setData == False: + list357.append(jaString) + + # Pass 2 + else: + if len(list357) > 0: + # Grab and Replace + translatedText = list357[0] + translatedText = jaString.replace(jaString, translatedText) + + # Remove characters that may break scripts + charList = ['"', "\\n"] + for char in charList: + translatedText = translatedText.replace(char, "") + + # Textwrap + translatedText = textwrap.fill(translatedText, WIDTH) + translatedText = translatedText.replace("- ", "-") + + # Center Text + if acExist: + translatedText = f'\\ac {translatedText.replace('\n', '\n\\ac ')}' + + # Check and Set Font + if "fontSize" in codeList[i]["parameters"][3]: + if font: + codeList[i]["parameters"][3]["fontSize"] = font + + # Set + codeList[i]["parameters"][3][argVar] = translatedText + list357.pop(0) + + ## Event Code: 657 [Picture Text] [Optional] + if "code" in codeList[i] and codeList[i]["code"] == 657 and CODE657 is True: + if "text" in codeList[i]["parameters"][0]: + jaString = codeList[i]["parameters"][0] + if not isinstance(jaString, str): + i += 1 + continue + + # Definitely don't want to mess with files + if "_" in jaString: + i += 1 + continue + + # If there isn't any Japanese in the text just skip + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", jaString): + i += 1 + continue + + # Remove outside text + startString = re.search(r"^[^一-龠ぁ-ゔァ-ヴー\<\>【】\\]+", jaString) + jaString = re.sub(r"^[^一-龠ぁ-ゔァ-ヴー\<\>【】\\]+", "", jaString) + endString = re.search(r"[^一-龠ぁ-ゔァ-ヴー\<\>【】。!?\\]+$", jaString) + jaString = re.sub(r"[^一-龠ぁ-ゔァ-ヴー\<\>【】。!?\\]+$", "", jaString) + if startString is None: + startString = "" + else: + startString = startString.group() + if endString is None: + endString = "" + else: + endString = endString.group() + + # Remove any textwrap + jaString = re.sub(r"\n", " ", jaString) + + # Translate + response = translateGPT(jaString, "", True) + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + translatedText = response[0] + + # Remove characters that may break scripts + charList = [".", '"', "'"] + for char in charList: + translatedText = translatedText.replace(char, "") + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + translatedText = startString + translatedText + endString + + # Set Data + codeList[i]["parameters"][0] = translatedText + + ## Event Code: 101 [Name] [Optional] + if "code" in codeList[i] and codeList[i]["code"] == 101 and CODE101 is True: + # Check Window Type (Certain games switch between 1st line speakers and none) + if FIRSTLINESPEAKERS: + if codeList[i]["parameters"][2] == 0: + speakerWindow = True + else: + speakerWindow = False + + else: + isVar = False + + # Grab String + jaString = "" + if len(codeList[i]["parameters"]) > 4: + jaString = codeList[i]["parameters"][4] + # Check for Var + elif len(codeList[i]["parameters"]) > 0: + jaString = codeList[i]["parameters"][0] + isVar = True + if not isinstance(jaString, str): + i += 1 + continue + + # Force Speaker using var + if "\\ap[1左]" in jaString.lower() or "\\ap[1右]" in jaString.lower(): + speaker = "Cecily" + i += 1 + continue + elif "\\ap[2左]" in jaString.lower() or "\\ap[2右]" in jaString.lower(): + speaker = "Amelia" + i += 1 + continue + elif "\\ap[3左]" in jaString.lower() or "\\ap[3右]" in jaString.lower(): + speaker = "Henry" + i += 1 + continue + elif "\\ap[4左]" in jaString.lower() or "\\ap[4右]" in jaString.lower(): + speaker = "Oswald" + i += 1 + continue + elif "\\ap" in jaString: + speaker = re.search(r"[\\]+AP\[(.*?)\]", jaString).group(1) + i += 1 + continue + + # Get Speaker + if "\\" not in jaString: + response = getSpeaker(jaString) + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + speaker = response[0] + + # Validate Speaker is not empty + if len(speaker) > 0: + if isVar == False: + codeList[i]["parameters"][4] = speaker + i += 1 + continue + else: + codeList[i]["parameters"][0] = speaker + isVar = False + i += 1 + continue + else: + speaker = "" + + ## Event Code: 355 or 655 Scripts [Optional] + if "code" in codeList[i] and (codeList[i]["code"] == 355 or codeList[i]["code"] == 655) and CODE355655 is True: + jaString = codeList[i]["parameters"][0] + regex = r'.*subject=(.*?)"' + + # Var Text + match = re.search(regex, jaString) + if re.search(regex, jaString): + finalJAString = match.group(1) + # Pass 1 + if setData is False: + list355655.append(finalJAString) + + # Pass 2 + else: + # Grab and Replace + translatedText = list355655[0] + translatedText = translatedText.replace("'", "\\'") + + # Set + codeList[i]["parameters"][0] = codeList[i]["parameters"][0].replace(finalJAString, translatedText) + list355655.pop(0) + + ## Event Code: 408 (Script) + if "code" in codeList[i] and (codeList[i]["code"] == 408) and CODE408 is True: + jaString = codeList[i]["parameters"][0] + + # If there isn't any Japanese in the text just skip + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", jaString): + i += 1 + continue + + if "secretText" in jaString: + regex = r"secretText:\s?(.+)" + elif "title" in jaString: + regex = r"title:\s?(.+)" + else: + regex = r"(.+)" + + # Need to remove outside code and put it back later + matchList = re.findall(regex, jaString) + + for match in matchList: + # Remove Textwrap + match = match.replace("\n", " ") + response = translateGPT( + match, + "Reply with the " + LANGUAGE + " translation of the achievement title.", + False, + ) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Replace + translatedText = jaString.replace(match, translatedText) + + # Remove characters that may break scripts + charList = [".", '"', "\\n"] + for char in charList: + translatedText = translatedText.replace(char, "") + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + + # Set Data + codeList[i]["parameters"][0] = translatedText + + ## Event Code: 108 (Script) + if "code" in codeList[i] and (codeList[i]["code"] == 108) and CODE108 is True: + jaString = codeList[i]["parameters"][0] + + # If there isn't any Japanese in the text just skip + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", jaString): + i += 1 + continue + + # Translate + if "info:" in jaString: + regex = r"info:(.*)" + elif "ActiveMessage:" in jaString: + regex = r"?" + elif "event_text" in jaString: + regex = r"event_text\s*:\s*(.*)" + else: + i += 1 + continue + + # Need to remove outside code and put it back later + match = re.search(regex, jaString) + if match: + # Pass 1 + if setData is False: + list108.append(match.group(1)) + + # Grab Next + j = i + while codeList[j + 1]["code"] == 408: + j += 1 + list108[0] = list108[0] + codeList[j]["parameters"][0].replace(">", "") + codeList[j]["parameters"][0] = "" + list108[0] = list108[0].replace("\n", " ") + + # Pass 2 + else: + # Grab and Replace + translatedText = list108[0] + list108.pop(0) + + # Textwrap + if codeList[i + 1]["code"] == 408: + translatedText = textwrap.fill(translatedText, WIDTH) + + # Remove characters that may break scripts + charList = ['"'] + for char in charList: + translatedText = translatedText.replace(char, "") + translatedText = translatedText.replace('"', '"') + translatedText = translatedText.replace(" ", "_") + translatedText = jaString.replace(match.group(1), translatedText) + + # Add > + if ">" not in translatedText: + translatedText = translatedText + ">" + + # Set Data + codeList[i]["parameters"][0] = translatedText + + ## Event Code: 356 + if "code" in codeList[i] and codeList[i]["code"] == 356 and CODE356 is True: + jaString = codeList[i]["parameters"][0] + oldjaString = jaString + + # Grab Speaker + if "Tachie showName" in jaString: + matchList = re.findall(r"Tachie showName (.+)", jaString) + if len(matchList) > 0: + # Translate + response = translateGPT( + matchList[0], + "Reply with the " + LANGUAGE + " translation of the NPC name.", + False, + ) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Set Text + speaker = translatedText + speaker = speaker.replace(" ", " ") + codeList[i]["parameters"][0] = jaString.replace(matchList[0], speaker) + i += 1 + continue + + # Want to translate this script + if "D_TEXT " in jaString: + regex = r"D_TEXT\s(.*?)\s.+" + elif "ShowInfo" in jaString: + regex = r"ShowInfo\s(.*)" + elif "PushGab" in jaString: + regex = r"PushGab\s(.*)" + elif "addLog" in jaString: + regex = r"addLog\s(.*)" + elif "DW_" in jaString: + regex = r"DW_.*?\s(.*)" + elif "CommonPopup" in jaString: + regex = r"CommonPopup\sadd\stext:(.*?)[\\]+}" + else: + regex = r"" + + # Remove any textwrap + jaString = re.sub(r"\n", "_", jaString) + + # Capture Arguments and text + textMatch = re.search(regex, jaString) + if textMatch and textMatch.group(0) != "": + text = textMatch.group(1) + + # Pass 1 + if setData == False: + text = text.replace("_", " ") + list356.append(text) + + # Pass 2 + else: + if len(list356) > 0: + # Grab + translatedText = list356[0] + + # Remove characters that may break scripts + charList = [".", '"'] + for char in charList: + translatedText = translatedText.replace(char, "") + + # Cant have spaces? + translatedText = translatedText.replace(" ", "_") + translatedText = translatedText.replace("__", "_") + + # Put Args Back + translatedText = jaString.replace(text, translatedText) + + # Set Data + codeList[i]["parameters"][0] = translatedText + list356.pop(0) + + if "namePop" in jaString: + matchList = re.findall(r"namePop\s\d+\s(.+?)\s.+", jaString) + if len(matchList) > 0: + # Translate + text = matchList[0] + response = translateGPT(text, "Reply with the " + LANGUAGE + " Translation", False) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Set Data + translatedText = jaString.replace(text, translatedText) + codeList[i]["parameters"][0] = translatedText + + if "LL_InfoPopupWIndowMV" in jaString: + matchList = re.findall(r"LL_InfoPopupWIndowMV\sshowWindow\s(.+?) .+", jaString) + if len(matchList) > 0: + # Translate + text = matchList[0] + response = translateGPT(text, "Reply with the " + LANGUAGE + " Translation", False) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Set Data + translatedText = translatedText.replace(" ", "_") + translatedText = jaString.replace(text, translatedText) + codeList[i]["parameters"][0] = translatedText + + if "OriginMenuStatus SetParam" in jaString: + matchList = re.findall(r"OriginMenuStatus\sSetParam\sparam[\d]\s(.*)", jaString) + if len(matchList) > 0: + # Translate + text = matchList[0] + response = translateGPT(text, "Reply with the " + LANGUAGE + " Translation", False) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Set Data + translatedText = translatedText.replace(" ", "_") + translatedText = jaString.replace(text, translatedText) + codeList[i]["parameters"][0] = translatedText + + # LL_GalgeChoiceWindowMV Message + if "LL_GalgeChoiceWindowMV setMessageText" in jaString: + ### Message Text First + match = re.search(r"LL_GalgeChoiceWindowMV setMessageText (.+)", jaString) + if match: + jaString = match.group(1) + + # Remove any textwrap & TL + jaString = re.sub(r"\n", " ", jaString) + response = translateGPT(jaString, "", False) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Textwrap & Replace Whitespace + translatedText = textwrap.fill(translatedText, width=WIDTH) + translatedText = translatedText.replace(" ", "_") + + # Replace and Set + translatedText = match.group(0).replace(match.group(1), translatedText) + codeList[i]["parameters"][0] = translatedText + + # LL_GalgeChoiceWindowMV Choices + if "LL_GalgeChoiceWindowMV setChoices": + match = re.search(r"LL_GalgeChoiceWindowMV setChoices (.+)", jaString) + if match: + jaString = match.group(1) + choiceList = jaString.split(",") + + # Translate + question = translatedText + response = translateGPT( + choiceList, + f"Previous text for context: {question}\n", + True, + ) + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + choiceListTL = response[0] + translatedText = match.group(0) + + # Replace Strings + for j in range(len(choiceListTL)): + choiceListTL[j] = choiceListTL[j].replace(" ", "_") + translatedText = translatedText.replace(choiceList[j], choiceListTL[j]) + + # Set Data + codeList[i]["parameters"][0] = translatedText + + ### Event Code: 102 Show Choice + if "code" in codeList[i] and codeList[i]["code"] == 102 and CODE102 is True: + choiceList = [] + varList = [] + for choice in range(len(codeList[i]["parameters"][0])): + jaString = codeList[i]["parameters"][0][choice] + jaString = jaString.replace(" 。", ".") + + # Avoid Empty Strings + if jaString == "": + continue + + # If and En Statements + ifVar = "" + ifList = re.findall(r"([ei][nf]\(.+?\)\)?\)?)", jaString) + if len(ifList) != 0: + for var in ifList: + jaString = jaString.replace(var, "") + ifVar += var + varList.append(ifVar) + + # Append to List + choiceList.append(jaString) + + # Translate + if len(textHistory) > 0: + response = translateGPT( + choiceList, + f"Previous text for context: {str(textHistory)}\n", + True, + ) + translatedTextList = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + else: + response = translateGPT(choiceList, "", True) + translatedTextList = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Check Mismatch + if len(translatedTextList) == len(choiceList): + for choice in range(len(codeList[i]["parameters"][0])): + jaString = codeList[i]["parameters"][0][choice] + jaString = jaString.replace(" 。", ".") + + # Avoid Empty Strings + if jaString == "": + continue + + translatedText = translatedTextList[choice] + + # Set Data + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + if translatedText != "": + translatedText = varList[choice] + translatedText[0].upper() + translatedText[1:] + else: + translatedText = varList[choice] + translatedText + codeList[i]["parameters"][0][choice] = translatedText + else: + if filename not in MISMATCH: + MISMATCH.append(filename) + + ### Event Code: 111 Script + if "code" in codeList[i] and codeList[i]["code"] == 111 and CODE111 is True: + for j in range(len(codeList[i]["parameters"])): + jaString = codeList[i]["parameters"][j] + + # Check if String + if not isinstance(jaString, str): + i += 1 + continue + + # Only TL the Game Variable + if "$gameVariables" not in jaString: + i += 1 + continue + + # This is going to be the var being set. (IMPORTANT) + if "1045" not in jaString: + i += 1 + continue + + # Need to remove outside code and put it back later + matchList = re.findall(r"'(.*?)'", jaString) + + for match in matchList: + response = translateGPT(match, "", False) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Remove characters that may break scripts + charList = [".", '"', "'", "\\n"] + for char in charList: + translatedText = translatedText.replace(char, "") + + jaString = jaString.replace(match, translatedText) + + # Set Data + translatedText = jaString + codeList[i]["parameters"][j] = translatedText + + ### Event Code: 320 Set Variable + if "code" in codeList[i] and codeList[i]["code"] == 320 and CODE320 is True: + jaString = codeList[i]["parameters"][1] + if not isinstance(jaString, str): + i += 1 + continue + + # Definitely don't want to mess with files + if "■" in jaString or "_" in jaString: + i += 1 + continue + + # If there isn't any Japanese in the text just skip + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", jaString): + i += 1 + continue + + # Translate + response = getSpeaker(jaString) + translatedText = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + + # Remove characters that may break scripts + charList = [".", '"', "'", "\\n"] + for char in charList: + translatedText = translatedText.replace(char, "") + + # Set Data + codeList[i]["parameters"][1] = translatedText + + # Iterate + else: + i += 1 + + # EOF + list401TL = [] + list122TL = [] + list356TL = [] + list357TL = [] + list355655TL = [] + list108TL = [] + setData = False + PBAR = pbar + + # 401 + if len(list401) > 0: + response = translateGPT(list401, "", True) + list401TL = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + if len(list401TL) != len(list401): + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + else: + setData = True + + # 122 + if len(list122) > 0: + response = translateGPT(list122, "Keep your translation as brief as possible", True) + list122TL = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + if len(list122TL) != len(list122): + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + else: + setData = True + + # 355/655 + if len(list355655) > 0: + response = translateGPT(list355655, textHistory, True) + list355655TL = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + if len(list355655TL) != len(list355655): + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + else: + setData = True + + # 108 + if len(list108) > 0: + response = translateGPT(list108, textHistory, True) + list108TL = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + if len(list108TL) != len(list108): + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + else: + setData = True + + # 356 + if len(list356) > 0: + response = translateGPT(list356, textHistory, True) + list356TL = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + if len(list356TL) != len(list356): + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + else: + setData = True + + # 357 + if len(list357) > 0: + response = translateGPT(list357, textHistory, True) + list357TL = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + if len(list357TL) != len(list357): + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + else: + setData = True + + # Start Pass 2 + if setData: + searchCodes( + page, + pbar, + [list401TL, list122TL, list355655TL, list108TL, list356TL, list357TL], + filename, + ) + + # Delete all -1 codes + codeListFinal = [] + for i in range(len(codeList)): + if "code" in codeList[i] and codeList[i]["code"] != -1: + codeListFinal.append(codeList[i]) + + # Normal Format + if "list" in page: + page["list"] = codeListFinal + + # Special Format (Scenario) + else: + page = codeListFinal + + except IndexError as e: + traceback.print_exc() + raise Exception(str(e) + "Failed to translate: " + oldjaString) from None + except Exception as e: + traceback.print_exc() + raise Exception(str(e) + "Failed to translate: " + oldjaString) from None + + return totalTokens + + +def searchSS(state, pbar): + totalTokens = [0, 0] + + # Name + nameResponse = ( + translateGPT( + state["name"], + "Reply with only the " + LANGUAGE + " translation of the RPG Skill name.", + False, + ) + if "name" in state + else "" + ) + + # Description + descriptionResponse = ( + translateGPT( + state["description"], + "Reply with only the " + LANGUAGE + " translation of the description.", + False, + ) + if "description" in state + else "" + ) + + # Messages + message1Response = "" + message4Response = "" + message2Response = "" + message3Response = "" + + if "message1" in state: + if len(state["message1"]) > 0 and state["message1"][0] in [ + "は", + "を", + "の", + "に", + "が", + ]: + message1Response = translateGPT( + "Taro" + state["message1"], + "reply with only the gender neutral " + + LANGUAGE + + " translation of the action log. Always start the sentence with Taro. For example,\ +Translate 'Taroを倒した!' as 'Taro was defeated!'", + False, + ) + else: + message1Response = translateGPT( + state["message1"], + "reply with only the gender neutral " + LANGUAGE + " translation", + False, + ) + + if "message2" in state: + if len(state["message2"]) > 0 and state["message2"][0] in [ + "は", + "を", + "の", + "に", + "が", + ]: + message2Response = translateGPT( + "Taro" + state["message2"], + "reply with only the gender neutral " + + LANGUAGE + + " translation of the action log. Always start the sentence with Taro. For example,\ +Translate 'Taroを倒した!' as 'Taro was defeated!'", + False, + ) + else: + message2Response = translateGPT( + state["message2"], + "reply with only the gender neutral " + LANGUAGE + " translation", + False, + ) + + if "message3" in state: + if len(state["message3"]) > 0 and state["message3"][0] in [ + "は", + "を", + "の", + "に", + "が", + ]: + message3Response = translateGPT( + "Taro" + state["message3"], + "reply with only the gender neutral " + + LANGUAGE + + " translation of the action log. Always start the sentence with Taro. For example,\ +Translate 'Taroを倒した!' as 'Taro was defeated!'", + False, + ) + else: + message3Response = translateGPT( + state["message3"], + "reply with only the gender neutral " + LANGUAGE + " translation", + False, + ) + + if "message4" in state: + if len(state["message4"]) > 0 and state["message4"][0] in [ + "は", + "を", + "の", + "に", + "が", + ]: + message4Response = translateGPT( + "Taro" + state["message4"], + "reply with only the gender neutral " + + LANGUAGE + + " translation of the action log. Always start the sentence with Taro. For example,\ +Translate 'Taroを倒した!' as 'Taro was defeated!'", + False, + ) + else: + message4Response = translateGPT( + state["message4"], + "reply with only the gender neutral " + LANGUAGE + " translation", + False, + ) + + # Translate State Notes + if "help" in state["note"]: + noteResponse = translateNote(state, r"]*)>") + totalTokens[0] += noteResponse[0] + totalTokens[1] += noteResponse[1] + if "STATE_HELP" in state["note"]: + noteResponse = translateNote(state, r"\n(.*)\n") + totalTokens[0] += noteResponse[0] + totalTokens[1] += noteResponse[1] + + # Count totalTokens + totalTokens[0] += nameResponse[1][0] if nameResponse != "" else 0 + totalTokens[1] += nameResponse[1][1] if nameResponse != "" else 0 + totalTokens[0] += descriptionResponse[1][0] if descriptionResponse != "" else 0 + totalTokens[1] += descriptionResponse[1][1] if descriptionResponse != "" else 0 + totalTokens[0] += message1Response[1][0] if message1Response != "" else 0 + totalTokens[1] += message1Response[1][1] if message1Response != "" else 0 + totalTokens[0] += message2Response[1][0] if message2Response != "" else 0 + totalTokens[1] += message2Response[1][1] if message2Response != "" else 0 + totalTokens[0] += message3Response[1][0] if message3Response != "" else 0 + totalTokens[1] += message3Response[1][1] if message3Response != "" else 0 + totalTokens[0] += message4Response[1][0] if message4Response != "" else 0 + totalTokens[1] += message4Response[1][1] if message4Response != "" else 0 + + # Set Data + if "name" in state: + state["name"] = nameResponse[0].replace('"', "") + if "description" in state: + # Textwrap + translatedText = descriptionResponse[0] + translatedText = textwrap.fill(translatedText, width=LISTWIDTH) + state["description"] = translatedText.replace('"', "") + if "message1" in state: + state["message1"] = message1Response[0].replace('"', "").replace("Taro", "") + if "message2" in state: + state["message2"] = message2Response[0].replace('"', "").replace("Taro", "") + if "message3" in state: + state["message3"] = message3Response[0].replace('"', "").replace("Taro", "") + if "message4" in state: + state["message4"] = message4Response[0].replace('"', "").replace("Taro", "") + + return totalTokens + + +def searchSystem(data, pbar): + totalTokens = [0, 0] + context = "Reply with only the " + LANGUAGE + ' translation of the UI textbox."' + + # Title + response = translateGPT( + data["gameTitle"], + " Reply with the " + LANGUAGE + " translation of the game title name", + False, + ) + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + data["gameTitle"] = response[0].strip(".") + + # Terms + for term in data["terms"]: + if term != "messages": + termList = data["terms"][term] + for i in range(len(termList)): # Last item is a messages object + if termList[i] is not None: + response = translateGPT(termList[i], context, False) + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + termList[i] = response[0].replace('"', "").strip() + + # Armor Types + for i in range(len(data["armorTypes"])): + response = translateGPT( + data["armorTypes"][i], + "Reply with only the " + LANGUAGE + " translation of the armor type", + False, + ) + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + data["armorTypes"][i] = response[0].replace('"', "").strip() + + # Skill Types + for i in range(len(data["skillTypes"])): + response = translateGPT( + data["skillTypes"][i], + "Reply with only the " + LANGUAGE + " translation", + False, + ) + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + data["skillTypes"][i] = response[0].replace('"', "").strip() + + # Equip Types + for i in range(len(data["equipTypes"])): + response = translateGPT( + data["equipTypes"][i], + "Reply with only the " + LANGUAGE + " translation of the equipment type. No disclaimers.", + False, + ) + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + data["equipTypes"][i] = response[0].replace('"', "").strip() + + # # Variables (Optional ususally) + # for i in range(len(data['variables'])): + # response = translateGPT(data['variables'][i], 'Reply with only the '+ LANGUAGE +' translation of the title', False) + # totalTokens[0] += response[1][0] + # totalTokens[1] += response[1][1] + # data['variables'][i] = response[0].replace('\"', '').strip() + + # Messages + messages = data["terms"]["messages"] + for key, value in messages.items(): + response = translateGPT( + value, + "Reply with only the " + + LANGUAGE + + ' translation of the battle text.\nTranslate "常時ダッシュ" as "Always Dash"\nTranslate "次の%1まで" as Next %1.', + False, + ) + translatedText = response[0] + + # Remove characters that may break scripts + charList = [".", '"', "\\n"] + for char in charList: + translatedText = translatedText.replace(char, "") + + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + messages[key] = translatedText + + return totalTokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + match speaker: + case "ファイン": + return ["Fine", [0, 0]] + case "": + return ["", [0, 0]] + case _: + # Find Speaker + for i in range(len(NAMESLIST)): + if speaker == NAMESLIST[i][0]: + return [NAMESLIST[i][1], [0, 0]] + + # Translate and Store Speaker + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + True, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + response[0] = response[0].replace("Speaker: ", "") + + # Retry if name doesn't translate for some reason + if re.search(r"([a-zA-Z??])", response[0]) == None: + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + False, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + + speakerList = [speaker, response[0]] + NAMESLIST.append(speakerList) + return response + return [speaker, [0, 0]] + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT, format): + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + if format == "json": + user = f"```json\n{subbedT}\n```" + else: + user = subbedT + return system, user + + +def translateText(system, user, history, penalty, format, model=MODEL): + # Prompt + msg = [{"role": "system", "content": system}] + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Response Format + if format == "json": + responseFormat = {"type": "json_object"} + else: + responseFormat = {"type": "text"} + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=model, + response_format=responseFormat, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "「": '\\"', + "」": '\\"', + "- ": "-", + "—": "―", + "】": "]", + "【": "[", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Remove Repeating Characters + pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") + translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + try: + translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) + translatedTextList = re.sub(r"(? instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False -PBAR = None - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 40 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handlePlugin(filename, estimate): - global ESTIMATE, PBAR - ESTIMATE = estimate - - if ESTIMATE: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - else: - try: - with open("translated/" + filename, "w", encoding="utf_8", errors="ignore") as outFile: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - outFile.writelines(translatedData[0]) - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - except Exception: - traceback.print_exc() - return "Fail" - - return getResultString(["", TOKENS, None], end - start, "TOTAL") - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="utf_8") as readFile: - translatedData = parsePlugin(readFile, filename) - - # Delete lines marked for deletion - finalData = [] - for line in translatedData[0]: - if line != "\\d\n": - finalData.append(line) - translatedData[0] = finalData - - return translatedData - - -def parsePlugin(readFile, filename): - totalTokens = [0, 0] - - # Read File into data - data = readFile.readlines() - - # Create Progress Bar - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - - try: - result = translatePlugin(data, pbar, filename, []) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translatePlugin(data, pbar, filename, translatedList): - stringList = [] - currentGroup = [] - tokens = [0, 0] - speaker = "" - voice = False - global LOCK, ESTIMATE - i = 0 - - while i < len(data): - voice = False - speaker = "" - newline = r"\n" - - """ - Plugin List - Quest Name: [\\]+"QuestName[\\]+":[\\]+"(.*?)[\\]+" - Quest Client: [\\]+"QuestClientName[\\]+":[\\]+"[\\]+"[\\]+"(.*?)[\\]+" - Quest Location: [\\]+"QuestionLocation[\\]+":[\\]+"[\\]+"[\\]+"(.*?)[\\]+" - Quest Targe Location: [\\]+"PlaceInformation[\\]+":[\\]+"(.*?)[\\]+" - Quest Summary: [\\]+"QuestContent[\\]+":[\\]+"(.*?)[\\]+" - Quest Goal: [\\]+"ObjectiveContent[\\]+":[\\]+"[\\]+"[\\]+"(.*?)[\\]+" - Quest Goal 2: [\\]+"ObjectiveContent[\\]+":[\\]+"[\\]+"[\\]+".*?[\\]+"(.*?)[\\]+" - - TODO TL all of the above in one call instead of multiple - """ - # Lines - regex = r'this.log_output\("(.*)"' - matchList = re.findall(regex, data[i]) - if len(matchList) > 0: - for match in matchList: - # Save Original String - originalString = match - - # Remove any textwrap - match = match.replace(newline, " ") - - # Pass 1 - if translatedList == []: - # Add String - if match != "\\\\\\\\": - stringList.append(match.strip()) - - # Pass 2 - else: - # Get Text - if translatedList: - # Grab and Pop - translatedText = translatedList[0] - translatedList.pop(0) - - # Set to None if empty list - if len(translatedList) <= 0: - translatedList = None - - # Textwrap - translatedText = textwrap.fill(translatedText, width=WIDTH) - translatedText = translatedText.replace("\n", newline) - - # Replace Single Quotes - translatedText = translatedText.replace("'", "\\'") - translatedText = translatedText.replace('"', "\\'") - - # Set Data - data[i] = data[i].replace(originalString, translatedText) - # Next Line - i += 1 - - # EOF - if len(stringList) > 0: - # Set Progress - pbar.total = len(stringList) - pbar.refresh() - PBAR = pbar - - # Translate - response = translateGPT(stringList, "", True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - translatedList = response[0] - - # Set Strings - if len(stringList) == len(translatedList): - translatePlugin(data, pbar, filename, translatedList) - - # Mismatch - else: - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - return tokens - - -# Save some money and enter the character before translation -def getSpeaker(speaker): - match speaker: - case "ファイン": - return ["Fine", [0, 0]] - case "": - return ["", [0, 0]] - case _: - # Find Speaker - for i in range(len(NAMESLIST)): - if speaker == NAMESLIST[i][0]: - return [NAMESLIST[i][1], [0, 0]] - - # Translate and Store Speaker - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - True, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - response[0] = response[0].replace("Speaker: ", "") - - # Retry if name doesn't translate for some reason - if re.search(r"([a-zA-Z??])", response[0]) == None: - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - - speakerList = [speaker, response[0]] - NAMESLIST.append(speakerList) - return response - return [speaker, [0, 0]] - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT, format): - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - if format == "json": - user = f"```json\n{subbedT}\n```" - else: - user = subbedT - return system, user - - -def translateText(system, user, history, penalty, format, model=MODEL): - # Prompt - msg = [{"role": "system", "content": system}] - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Response Format - if format == "json": - responseFormat = {"type": "json_object"} - else: - responseFormat = {"type": "text"} - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=model, - response_format=responseFormat, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "「": '\\"', - "」": '\\"', - "- ": "-", - "—": "―", - "】": "]", - "【": "[", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Remove Repeating Characters - pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") - translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - try: - translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) - translatedTextList = re.sub(r"(? instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False +PBAR = None + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 40 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handlePlugin(filename, estimate): + global ESTIMATE, PBAR + ESTIMATE = estimate + + if ESTIMATE: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + else: + try: + with open("translated/" + filename, "w", encoding="utf_8", errors="ignore") as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + outFile.writelines(translatedData[0]) + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + except Exception: + traceback.print_exc() + return "Fail" + + return getResultString(["", TOKENS, None], end - start, "TOTAL") + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="utf_8") as readFile: + translatedData = parsePlugin(readFile, filename) + + # Delete lines marked for deletion + finalData = [] + for line in translatedData[0]: + if line != "\\d\n": + finalData.append(line) + translatedData[0] = finalData + + return translatedData + + +def parsePlugin(readFile, filename): + totalTokens = [0, 0] + + # Read File into data + data = readFile.readlines() + + # Create Progress Bar + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + + try: + result = translatePlugin(data, pbar, filename, []) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translatePlugin(data, pbar, filename, translatedList): + stringList = [] + currentGroup = [] + tokens = [0, 0] + speaker = "" + voice = False + global LOCK, ESTIMATE + i = 0 + + while i < len(data): + voice = False + speaker = "" + newline = r"\n" + + """ + Plugin List + Quest Name: [\\]+"QuestName[\\]+":[\\]+"(.*?)[\\]+" + Quest Client: [\\]+"QuestClientName[\\]+":[\\]+"[\\]+"[\\]+"(.*?)[\\]+" + Quest Location: [\\]+"QuestionLocation[\\]+":[\\]+"[\\]+"[\\]+"(.*?)[\\]+" + Quest Targe Location: [\\]+"PlaceInformation[\\]+":[\\]+"(.*?)[\\]+" + Quest Summary: [\\]+"QuestContent[\\]+":[\\]+"(.*?)[\\]+" + Quest Goal: [\\]+"ObjectiveContent[\\]+":[\\]+"[\\]+"[\\]+"(.*?)[\\]+" + Quest Goal 2: [\\]+"ObjectiveContent[\\]+":[\\]+"[\\]+"[\\]+".*?[\\]+"(.*?)[\\]+" + + TODO TL all of the above in one call instead of multiple + """ + # Lines + regex = r'this.log_output\("(.*)"' + matchList = re.findall(regex, data[i]) + if len(matchList) > 0: + for match in matchList: + # Save Original String + originalString = match + + # Remove any textwrap + match = match.replace(newline, " ") + + # Pass 1 + if translatedList == []: + # Add String + if match != "\\\\\\\\": + stringList.append(match.strip()) + + # Pass 2 + else: + # Get Text + if translatedList: + # Grab and Pop + translatedText = translatedList[0] + translatedList.pop(0) + + # Set to None if empty list + if len(translatedList) <= 0: + translatedList = None + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + translatedText = translatedText.replace("\n", newline) + + # Replace Single Quotes + translatedText = translatedText.replace("'", "\\'") + translatedText = translatedText.replace('"', "\\'") + + # Set Data + data[i] = data[i].replace(originalString, translatedText) + # Next Line + i += 1 + + # EOF + if len(stringList) > 0: + # Set Progress + pbar.total = len(stringList) + pbar.refresh() + PBAR = pbar + + # Translate + response = translateGPT(stringList, "", True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedList = response[0] + + # Set Strings + if len(stringList) == len(translatedList): + translatePlugin(data, pbar, filename, translatedList) + + # Mismatch + else: + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + return tokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + match speaker: + case "ファイン": + return ["Fine", [0, 0]] + case "": + return ["", [0, 0]] + case _: + # Find Speaker + for i in range(len(NAMESLIST)): + if speaker == NAMESLIST[i][0]: + return [NAMESLIST[i][1], [0, 0]] + + # Translate and Store Speaker + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + True, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + response[0] = response[0].replace("Speaker: ", "") + + # Retry if name doesn't translate for some reason + if re.search(r"([a-zA-Z??])", response[0]) == None: + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + False, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + + speakerList = [speaker, response[0]] + NAMESLIST.append(speakerList) + return response + return [speaker, [0, 0]] + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT, format): + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + if format == "json": + user = f"```json\n{subbedT}\n```" + else: + user = subbedT + return system, user + + +def translateText(system, user, history, penalty, format, model=MODEL): + # Prompt + msg = [{"role": "system", "content": system}] + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Response Format + if format == "json": + responseFormat = {"type": "json_object"} + else: + responseFormat = {"type": "text"} + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=model, + response_format=responseFormat, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "「": '\\"', + "」": '\\"', + "- ": "-", + "—": "―", + "】": "]", + "【": "[", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Remove Repeating Characters + pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") + translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + try: + translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) + translatedTextList = re.sub(r"(? instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) -FILENAME = None - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False -PBAR = None - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 30 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleText(filename, estimate): - global ESTIMATE, TOKENS, FILENAME - ESTIMATE = estimate - FILENAME = filename - - # Translate - start = time.time() - translatedData = openFiles(filename) - - # Translate - if not estimate: - try: - with open("translated/" + filename, "w", encoding="utf-8") as outFile: - outFile.writelines(translatedData[0]) - except Exception: - traceback.print_exc() - return "Fail" - - # Print File - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="utf8") as readFile: - translatedData = parseText(readFile, filename) - - # Delete lines marked for deletion - finalData = [] - for line in translatedData[0]: - if line != "\\d\n": - finalData.append(line) - translatedData[0] = finalData - - return translatedData - - -def parseText(readFile, filename): - global PBAR - totalTokens = [0, 0] - - # Read File into data - data = readFile.readlines() - - # Create Progress Bar - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - PBAR = pbar - - try: - result = translateTxt(data, []) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateTxt(data, translatedList): - if translatedList: - stringList = translatedList[0] - choiceList = translatedList[1] - else: - stringList = [] - choiceList = [] - tokens = [0, 0] - speaker = "" - global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH - i = 0 - - while i < len(data): - lineTextRegex = r"(.+)" - - # Dialogue - match = re.search(lineTextRegex, data[i]) - jaString = None - if match: - # Set String - jaString = match.group(1) - - # Pass 1 - if not translatedList: - # Strip Spaces - jaString = jaString.strip() - - if jaString: - if speaker: - stringList.append(f"[{speaker}]: {jaString}") - else: - stringList.append(jaString) - - # Pass 2 - else: - # Get Text - if stringList: - # Grab and Pop - translatedText = stringList[0] - stringList.pop(0) - - # Set to None if empty list - if len(stringList) <= 0: - stringList = None - - # Remove speaker - translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText) - - # # Textwrap - # translatedText = textwrap.fill(translatedText, width=WIDTH) - # translatedText = translatedText.replace("\n", "\\n") - - # Set Data - data[i] = f"{translatedText}\n" - - i += 1 - else: - i += 1 - - # EOF - if not translatedList: - stringListTL = [] - choiceListTL = [] - - # String List - if stringList: - PBAR.total = len(stringList) - PBAR.refresh() - response = translateGPT(stringList, "Reply with the English Translation", True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - stringListTL = response[0] - - if len(stringList) != len(stringListTL): - # Mismatch - with LOCK: - if FILENAME not in MISMATCH: - MISMATCH.append(FILENAME) - - # Choice List - if choiceList: - response = translateGPT(choiceList, "Reply with the English TL of the Dialogue Choice", True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - choiceListTL = response[0] - - if len(choiceList) != len(choiceListTL): - # Mismatch - with LOCK: - if FILENAME not in MISMATCH: - MISMATCH.append(FILENAME) - - # Set Strings - translateTxt(data, [stringListTL, choiceListTL]) - return tokens - - -# Save some money and enter the character before translation -def getSpeaker(speaker): - match speaker: - case "ファイン": - return ["Fine", [0, 0]] - case "": - return ["", [0, 0]] - case _: - # Find Speaker - for i in range(len(NAMESLIST)): - if speaker == NAMESLIST[i][0]: - return [NAMESLIST[i][1], [0, 0]] - - # Translate and Store Speaker - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - True, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - response[0] = response[0].replace("Speaker: ", "") - - # Retry if name doesn't translate for some reason - if re.search(r"([a-zA-Z??])", response[0]) == None: - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - - speakerList = [speaker, response[0]] - NAMESLIST.append(speakerList) - return response - return [speaker, [0, 0]] - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT, format): - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - if format == "json": - user = f"```json\n{subbedT}\n```" - else: - user = subbedT - return system, user - - -def translateText(system, user, history, penalty, format, model=MODEL): - # Prompt - msg = [{"role": "system", "content": system}] - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Response Format - if format == "json": - responseFormat = {"type": "json_object"} - else: - responseFormat = {"type": "text"} - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=model, - response_format=responseFormat, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "「": '\\"', - "」": '\\"', - "- ": "-", - "—": "―", - "】": "]", - "【": "[", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Remove Repeating Characters - pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") - translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - try: - translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) - translatedTextList = re.sub(r"(? instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) +FILENAME = None + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False +PBAR = None + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 30 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleText(filename, estimate): + global ESTIMATE, TOKENS, FILENAME + ESTIMATE = estimate + FILENAME = filename + + # Translate + start = time.time() + translatedData = openFiles(filename) + + # Translate + if not estimate: + try: + with open("translated/" + filename, "w", encoding="utf-8") as outFile: + outFile.writelines(translatedData[0]) + except Exception: + traceback.print_exc() + return "Fail" + + # Print File + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="utf8") as readFile: + translatedData = parseText(readFile, filename) + + # Delete lines marked for deletion + finalData = [] + for line in translatedData[0]: + if line != "\\d\n": + finalData.append(line) + translatedData[0] = finalData + + return translatedData + + +def parseText(readFile, filename): + global PBAR + totalTokens = [0, 0] + + # Read File into data + data = readFile.readlines() + + # Create Progress Bar + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + PBAR = pbar + + try: + result = translateTxt(data, []) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateTxt(data, translatedList): + if translatedList: + stringList = translatedList[0] + choiceList = translatedList[1] + else: + stringList = [] + choiceList = [] + tokens = [0, 0] + speaker = "" + global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH + i = 0 + + while i < len(data): + lineTextRegex = r"(.+)" + + # Dialogue + match = re.search(lineTextRegex, data[i]) + jaString = None + if match: + # Set String + jaString = match.group(1) + + # Pass 1 + if not translatedList: + # Strip Spaces + jaString = jaString.strip() + + if jaString: + if speaker: + stringList.append(f"[{speaker}]: {jaString}") + else: + stringList.append(jaString) + + # Pass 2 + else: + # Get Text + if stringList: + # Grab and Pop + translatedText = stringList[0] + stringList.pop(0) + + # Set to None if empty list + if len(stringList) <= 0: + stringList = None + + # Remove speaker + translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText) + + # # Textwrap + # translatedText = textwrap.fill(translatedText, width=WIDTH) + # translatedText = translatedText.replace("\n", "\\n") + + # Set Data + data[i] = f"{translatedText}\n" + + i += 1 + else: + i += 1 + + # EOF + if not translatedList: + stringListTL = [] + choiceListTL = [] + + # String List + if stringList: + PBAR.total = len(stringList) + PBAR.refresh() + response = translateGPT(stringList, "Reply with the English Translation", True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + stringListTL = response[0] + + if len(stringList) != len(stringListTL): + # Mismatch + with LOCK: + if FILENAME not in MISMATCH: + MISMATCH.append(FILENAME) + + # Choice List + if choiceList: + response = translateGPT(choiceList, "Reply with the English TL of the Dialogue Choice", True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + choiceListTL = response[0] + + if len(choiceList) != len(choiceListTL): + # Mismatch + with LOCK: + if FILENAME not in MISMATCH: + MISMATCH.append(FILENAME) + + # Set Strings + translateTxt(data, [stringListTL, choiceListTL]) + return tokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + match speaker: + case "ファイン": + return ["Fine", [0, 0]] + case "": + return ["", [0, 0]] + case _: + # Find Speaker + for i in range(len(NAMESLIST)): + if speaker == NAMESLIST[i][0]: + return [NAMESLIST[i][1], [0, 0]] + + # Translate and Store Speaker + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + True, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + response[0] = response[0].replace("Speaker: ", "") + + # Retry if name doesn't translate for some reason + if re.search(r"([a-zA-Z??])", response[0]) == None: + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + False, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + + speakerList = [speaker, response[0]] + NAMESLIST.append(speakerList) + return response + return [speaker, [0, 0]] + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT, format): + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + if format == "json": + user = f"```json\n{subbedT}\n```" + else: + user = subbedT + return system, user + + +def translateText(system, user, history, penalty, format, model=MODEL): + # Prompt + msg = [{"role": "system", "content": system}] + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Response Format + if format == "json": + responseFormat = {"type": "json_object"} + else: + responseFormat = {"type": "text"} + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=model, + response_format=responseFormat, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "「": '\\"', + "」": '\\"', + "- ": "-", + "—": "―", + "】": "]", + "【": "[", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Remove Repeating Characters + pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") + translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + try: + translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) + translatedTextList = re.sub(r"(? instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) -FILENAME = None - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False -PBAR = None - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 20 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleTyrano(filename, estimate): - global ESTIMATE, TOKENS, FILENAME - ESTIMATE = estimate - FILENAME = filename - - # Translate - start = time.time() - translatedData = openFiles(filename) - - # Translate - if not estimate: - try: - with open("translated/" + filename, "w", encoding="utf-8") as outFile: - outFile.writelines(translatedData[0]) - except Exception: - traceback.print_exc() - return "Fail" - - # Print File - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="utf8") as readFile: - translatedData = parseTyrano(readFile, filename) - - # Delete lines marked for deletion - finalData = [] - for line in translatedData[0]: - if line != "\\d\n": - finalData.append(line) - translatedData[0] = finalData - - return translatedData - - -def parseTyrano(readFile, filename): - global PBAR - totalTokens = [0, 0] - - # Read File into data - data = readFile.readlines() - - # Create Progress Bar - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - PBAR = pbar - - try: - result = translateTyrano(data, []) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateTyrano(data, translatedList): - if translatedList: - stringList = translatedList[0] - choiceList = translatedList[1] - else: - stringList = [] - choiceList = [] - tokens = [0, 0] - speaker = "" - global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH - i = 0 - - while i < len(data): - voice = False - lineRegexNoSpeaker = r"^([^\[#;*@\n]+)\[l\]\[[rp]\]|^([^\[#;*@\n]+)\[[rpl]\]|^([^\[#;*@_\n]+)\n$" - lineRegexSpeaker = r"^#(.*)" - furiganaRegex = r"(\[ruby\stext=(.*?)\])" - choiceRegex = r'\[glink.+?text="(.*?)"' - - # Speaker - match = re.search(lineRegexSpeaker, data[i]) - if match: - if match.group(1): - response = getSpeaker(match.group(1)) - speaker = response[0] - tokens[0] += response[1][0] - tokens[1] += response[1][1] - data[i] = data[i].replace(match.group(1), speaker) - else: - speaker = None - - # Furigana - match = re.search(r"^\[ruby\stext", data[i]) - furiganaList = [] - if match: - # Check next line and combine - while match: - furiganaList.append(data[i].replace("\n", "")) - del data[i] - match = re.search(r"^\[ruby\stext", data[i]) - jaString = "".join(furiganaList) - - # Ruby Text - furiganaList = re.findall(furiganaRegex, jaString) - for furigana in furiganaList: - jaString = jaString.replace(furigana[0], furigana[1]) - - data.insert(i, f"{jaString}[r]") - - # Dialogue - match = re.search(lineRegexNoSpeaker, data[i]) - jaString = None - if match: - jaString = match.group(1) - if not jaString: - jaString = match.group(2) - if not jaString: - jaString = match.group(3) - - originalString = jaString - - # Pass 1 - if not translatedList: - # Remove any textwrap and commands - jaString = jaString.replace("[r]", " ") - jaString = jaString.replace("[l]", "") - - # Ruby Text - furiganaList = re.findall(furiganaRegex, jaString) - for furigana in furiganaList: - jaString = jaString.replace(furigana[0], furigana[1]) - - # Strip Spaces - jaString = jaString.strip() - - if jaString: - if speaker: - stringList.append(f"[{speaker}]: {jaString}") - else: - stringList.append(jaString) - - # Pass 2 - else: - # Get Text - if stringList: - # Grab and Pop - translatedText = stringList[0] - stringList.pop(0) - - # Set to None if empty list - if len(stringList) <= 0: - stringList = None - - # Remove speaker - if speaker != "": - matchSpeakerList = re.findall(r"^\[?(.+?)\]?\s?[|:]\s?", translatedText) - translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText) - - # # Textwrap - # translatedText = textwrap.fill(translatedText, width=WIDTH) - # translatedText = translatedText.replace('\n', '[r]') - - # Avoid Crashes - translatedText = translatedText.replace("[", "(") - translatedText = translatedText.replace("]", ")") - - # Set Data - data[i] = data[i].replace(originalString, translatedText) - - # Choices - match = re.search(choiceRegex, data[i]) - if match: - # Pass 1 - if not translatedList: - choiceList.append(match.group(1)) - match = re.search(choiceRegex, data[i + 1]) - - # Pass 2 - else: - # Grab and Pop - translatedText = choiceList[0] - choiceList.pop(0) - - # Replace Spaces - translatedText = translatedText.replace(" ", "\u3000") - - # Set - data[i] = data[i].replace(match.group(1), translatedText) - - i += 1 - else: - i += 1 - - # EOF - if not translatedList: - stringListTL = [] - choiceListTL = [] - - # String List - if stringList: - PBAR.total = len(stringList) - PBAR.refresh() - response = translateGPT(stringList, "Reply with the English Translation", True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - stringListTL = response[0] - - if len(stringList) != len(stringListTL): - # Mismatch - with LOCK: - if FILENAME not in MISMATCH: - MISMATCH.append(FILENAME) - - # Choice List - if choiceList: - response = translateGPT(choiceList, "Reply with the English TL of the Dialogue Choice", True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - choiceListTL = response[0] - - if len(choiceList) != len(choiceListTL): - # Mismatch - with LOCK: - if FILENAME not in MISMATCH: - MISMATCH.append(FILENAME) - - # Set Strings - translateTyrano(data, [stringListTL, choiceListTL]) - return tokens - - -# Save some money and enter the character before translation -def getSpeaker(speaker): - match speaker: - case "ファイン": - return ["Fine", [0, 0]] - case "": - return ["", [0, 0]] - case _: - # Find Speaker - for i in range(len(NAMESLIST)): - if speaker == NAMESLIST[i][0]: - return [NAMESLIST[i][1], [0, 0]] - - # Translate and Store Speaker - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - True, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - response[0] = response[0].replace("Speaker: ", "") - - # Retry if name doesn't translate for some reason - if re.search(r"([a-zA-Z??])", response[0]) == None: - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - - speakerList = [speaker, response[0]] - NAMESLIST.append(speakerList) - return response - return [speaker, [0, 0]] - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT, format): - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - if format == "json": - user = f"```json\n{subbedT}\n```" - else: - user = subbedT - return system, user - - -def translateText(system, user, history, penalty, format, model=MODEL): - # Prompt - msg = [{"role": "system", "content": system}] - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Response Format - if format == "json": - responseFormat = {"type": "json_object"} - else: - responseFormat = {"type": "text"} - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=model, - response_format=responseFormat, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "「": '\\"', - "」": '\\"', - "- ": "-", - "—": "―", - "】": "]", - "【": "[", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Remove Repeating Characters - pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") - translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - try: - translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) - translatedTextList = re.sub(r"(? instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) +FILENAME = None + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False +PBAR = None + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 20 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleTyrano(filename, estimate): + global ESTIMATE, TOKENS, FILENAME + ESTIMATE = estimate + FILENAME = filename + + # Translate + start = time.time() + translatedData = openFiles(filename) + + # Translate + if not estimate: + try: + with open("translated/" + filename, "w", encoding="utf-8") as outFile: + outFile.writelines(translatedData[0]) + except Exception: + traceback.print_exc() + return "Fail" + + # Print File + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="utf8") as readFile: + translatedData = parseTyrano(readFile, filename) + + # Delete lines marked for deletion + finalData = [] + for line in translatedData[0]: + if line != "\\d\n": + finalData.append(line) + translatedData[0] = finalData + + return translatedData + + +def parseTyrano(readFile, filename): + global PBAR + totalTokens = [0, 0] + + # Read File into data + data = readFile.readlines() + + # Create Progress Bar + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + PBAR = pbar + + try: + result = translateTyrano(data, []) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateTyrano(data, translatedList): + if translatedList: + stringList = translatedList[0] + choiceList = translatedList[1] + else: + stringList = [] + choiceList = [] + tokens = [0, 0] + speaker = "" + global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH + i = 0 + + while i < len(data): + voice = False + lineRegexNoSpeaker = r"^([^\[#;*@\n]+)\[l\]\[[rp]\]|^([^\[#;*@\n]+)\[[rpl]\]|^([^\[#;*@_\n]+)\n$" + lineRegexSpeaker = r"^#(.*)" + furiganaRegex = r"(\[ruby\stext=(.*?)\])" + choiceRegex = r'\[glink.+?text="(.*?)"' + + # Speaker + match = re.search(lineRegexSpeaker, data[i]) + if match: + if match.group(1): + response = getSpeaker(match.group(1)) + speaker = response[0] + tokens[0] += response[1][0] + tokens[1] += response[1][1] + data[i] = data[i].replace(match.group(1), speaker) + else: + speaker = None + + # Furigana + match = re.search(r"^\[ruby\stext", data[i]) + furiganaList = [] + if match: + # Check next line and combine + while match: + furiganaList.append(data[i].replace("\n", "")) + del data[i] + match = re.search(r"^\[ruby\stext", data[i]) + jaString = "".join(furiganaList) + + # Ruby Text + furiganaList = re.findall(furiganaRegex, jaString) + for furigana in furiganaList: + jaString = jaString.replace(furigana[0], furigana[1]) + + data.insert(i, f"{jaString}[r]") + + # Dialogue + match = re.search(lineRegexNoSpeaker, data[i]) + jaString = None + if match: + jaString = match.group(1) + if not jaString: + jaString = match.group(2) + if not jaString: + jaString = match.group(3) + + originalString = jaString + + # Pass 1 + if not translatedList: + # Remove any textwrap and commands + jaString = jaString.replace("[r]", " ") + jaString = jaString.replace("[l]", "") + + # Ruby Text + furiganaList = re.findall(furiganaRegex, jaString) + for furigana in furiganaList: + jaString = jaString.replace(furigana[0], furigana[1]) + + # Strip Spaces + jaString = jaString.strip() + + if jaString: + if speaker: + stringList.append(f"[{speaker}]: {jaString}") + else: + stringList.append(jaString) + + # Pass 2 + else: + # Get Text + if stringList: + # Grab and Pop + translatedText = stringList[0] + stringList.pop(0) + + # Set to None if empty list + if len(stringList) <= 0: + stringList = None + + # Remove speaker + if speaker != "": + matchSpeakerList = re.findall(r"^\[?(.+?)\]?\s?[|:]\s?", translatedText) + translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText) + + # # Textwrap + # translatedText = textwrap.fill(translatedText, width=WIDTH) + # translatedText = translatedText.replace('\n', '[r]') + + # Avoid Crashes + translatedText = translatedText.replace("[", "(") + translatedText = translatedText.replace("]", ")") + + # Set Data + data[i] = data[i].replace(originalString, translatedText) + + # Choices + match = re.search(choiceRegex, data[i]) + if match: + # Pass 1 + if not translatedList: + choiceList.append(match.group(1)) + match = re.search(choiceRegex, data[i + 1]) + + # Pass 2 + else: + # Grab and Pop + translatedText = choiceList[0] + choiceList.pop(0) + + # Replace Spaces + translatedText = translatedText.replace(" ", "\u3000") + + # Set + data[i] = data[i].replace(match.group(1), translatedText) + + i += 1 + else: + i += 1 + + # EOF + if not translatedList: + stringListTL = [] + choiceListTL = [] + + # String List + if stringList: + PBAR.total = len(stringList) + PBAR.refresh() + response = translateGPT(stringList, "Reply with the English Translation", True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + stringListTL = response[0] + + if len(stringList) != len(stringListTL): + # Mismatch + with LOCK: + if FILENAME not in MISMATCH: + MISMATCH.append(FILENAME) + + # Choice List + if choiceList: + response = translateGPT(choiceList, "Reply with the English TL of the Dialogue Choice", True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + choiceListTL = response[0] + + if len(choiceList) != len(choiceListTL): + # Mismatch + with LOCK: + if FILENAME not in MISMATCH: + MISMATCH.append(FILENAME) + + # Set Strings + translateTyrano(data, [stringListTL, choiceListTL]) + return tokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + match speaker: + case "ファイン": + return ["Fine", [0, 0]] + case "": + return ["", [0, 0]] + case _: + # Find Speaker + for i in range(len(NAMESLIST)): + if speaker == NAMESLIST[i][0]: + return [NAMESLIST[i][1], [0, 0]] + + # Translate and Store Speaker + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + True, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + response[0] = response[0].replace("Speaker: ", "") + + # Retry if name doesn't translate for some reason + if re.search(r"([a-zA-Z??])", response[0]) == None: + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + False, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + + speakerList = [speaker, response[0]] + NAMESLIST.append(speakerList) + return response + return [speaker, [0, 0]] + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT, format): + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + if format == "json": + user = f"```json\n{subbedT}\n```" + else: + user = subbedT + return system, user + + +def translateText(system, user, history, penalty, format, model=MODEL): + # Prompt + msg = [{"role": "system", "content": system}] + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Response Format + if format == "json": + responseFormat = {"type": "json_object"} + else: + responseFormat = {"type": "text"} + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=model, + response_format=responseFormat, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "「": '\\"', + "」": '\\"', + "- ": "-", + "—": "―", + "】": "]", + "【": "[", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Remove Repeating Characters + pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") + translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + try: + translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) + translatedTextList = re.sub(r"(? instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False -PBAR = None -FILENAME = None - -# Full Width -ascii_to_wide = dict((i, chr(i + 0xFEE0)) for i in range(0x21, 0x7F)) -ascii_to_wide.update({0x20: "\u3000", 0x2D: "\u2212"}) # space and minus -wide_to_ascii = dict((i, chr(i - 0xFEE0)) for i in range(0xFF01, 0xFF5F)) -wide_to_ascii.update({0x3000: " ", 0x2212: "-"}) # space and minus - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 40 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleUnity(filename, estimate): - global ESTIMATE, FILENAME - ESTIMATE = estimate - FILENAME = filename - - if ESTIMATE: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - else: - try: - with open("translated/" + filename, "w", encoding="utf8", errors="ignore") as outFile: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - outFile.writelines(translatedData[0]) - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - except Exception: - traceback.print_exc() - return "Fail" - - return getResultString(["", TOKENS, None], end - start, "TOTAL") - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="utf8") as readFile: - translatedData = parseUnity(readFile, filename) - - # Delete lines marked for deletion - finalData = [] - for line in translatedData[0]: - if line != "\\d\n": - finalData.append(line) - translatedData[0] = finalData - - return translatedData - - -def parseUnity(readFile, filename): - totalTokens = [0, 0] - - # Read File into data - data = readFile.readlines() - - # Create Progress Bar - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - - try: - result = translateUnity(data, pbar, filename, []) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateUnity(data, pbar, filename, translatedList): - stringList = [] - currentGroup = [] - tokens = [0, 0] - speaker = "" - voice = False - global LOCK, ESTIMATE, PBAR - PBAR = pbar - i = 0 - - # Dialogue - while i < len(data): - # Lines - regex = r".*?=(.*)" - match = re.search(regex, data[i]) - if match != None and match.group(1) != "": - originalString = match.group(1) - # Pass 1 - if translatedList == []: - # Grab Consecutive Strings - jaString = match.group(1) - - # Remove textwrap - jaString = jaString.replace("\n", "") - - # Add String - stringList.append(jaString.strip()) - - # Pass 2 - else: - # Get Text - if translatedList: - # Grab and Pop - translatedText = translatedList[0] - translatedList.pop(0) - - # Set to None if empty list - if len(translatedList) <= 0: - translatedList = None - - # Textwrap - translatedText = textwrap.fill(translatedText, width=WIDTH) - translatedText = translatedText.replace("\n", "\\n") - - # Remove Double Spaces and = - translatedText = translatedText.replace(" ", " ") - translatedText = translatedText.replace("=", "->") - - # Set Data - data[i] = f"{originalString}{originalString}={translatedText}\n" - i += 1 - - # Nothing relevant. Skip Line. - else: - i += 1 - - # EOF - if len(stringList) > 0: - # Set Progress - pbar.total = len(stringList) - pbar.refresh() - - # Translate - response = translateGPT(stringList, "", True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - translatedList = response[0] - - # Set Strings - if len(stringList) == len(translatedList): - translateUnity(data, pbar, filename, translatedList) - - # Mismatch - else: - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - return tokens - - -# Save some money and enter the character before translation -def getSpeaker(speaker): - if speaker not in str(NAMESLIST): - response = translateGPT( - speaker, - "Reply with the " + LANGUAGE + " translation of the NPC name.", - True, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - - # Retry if name doesn't translate for some reason - if re.search(r"([a-zA-Z??])", response[0]) == None: - response = translateGPT( - speaker, - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - - speakerList = [speaker, response[0]] - NAMESLIST.append(speakerList) - return response - - # Find Speaker - else: - for i in range(len(NAMESLIST)): - if speaker == NAMESLIST[i][0]: - return [NAMESLIST[i][1], [0, 0]] - - return [speaker, [0, 0]] - - -def subVars(jaString): - jaString = jaString.replace("\u3000", " ") - - # Formatting - count = 0 - codeList = re.findall(r"[\\]+[\w]+\[[a-zA-Z0-9\\\[\]\_,\s-]+\]", jaString) - codeList = set(codeList) - if len(codeList) != 0: - for var in codeList: - jaString = jaString.replace(var, "[FCode_" + str(count) + "]") - count += 1 - - # Put all lists in list and return - return [jaString, codeList] - - -def resubVars(translatedText, codeList): - # Fix Spacing and ChatGPT Nonsense - matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) - if len(matchList) > 0: - for match in matchList: - text = match.strip() - translatedText = translatedText.replace(match, text) - - # Formatting - count = 0 - if len(codeList) != 0: - for var in codeList: - translatedText = translatedText.replace("[FCode_" + str(count) + "]", var) - count += 1 - - return translatedText - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT, format): - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - if format == "json": - user = f"```json\n{subbedT}\n```" - else: - user = subbedT - return system, user - - -def translateText(system, user, history, penalty, format, model=MODEL): - # Prompt - msg = [{"role": "system", "content": system}] - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Response Format - if format == "json": - responseFormat = {"type": "json_object"} - else: - responseFormat = {"type": "text"} - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=model, - response_format=responseFormat, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "「": '\\"', - "」": '\\"', - "- ": "-", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - translatedText = resubVars(translatedText, varResponse[1]) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - try: - line_dict = json.loads(translatedTextList) - # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. - string_list = list(line_dict.values()) - if is_list: - return string_list - else: - return string_list[0] - - except Exception as e: - print(f"extractTranslation Error: {e}") - return None - - -def countTokens(system, user, history): - inputTotalTokens = 0 - outputTotalTokens = 0 - enc = tiktoken.encoding_for_model("gpt-4") - - # Input - if isinstance(history, list): - for line in history: - inputTotalTokens += len(enc.encode(line)) - else: - inputTotalTokens += len(enc.encode(history)) - inputTotalTokens += len(enc.encode(system)) - inputTotalTokens += len(enc.encode(user)) - - # Output - outputTotalTokens += round(len(enc.encode(user)) * 3) - - return [inputTotalTokens, outputTotalTokens] - - -@retry(exceptions=Exception, tries=5, delay=5) -def translateGPT(text, history, fullPromptFlag): - global PBAR, MISMATCH, FILENAME - - mismatch = False - totalTokens = [0, 0] - if isinstance(text, list): - format = "json" - tList = batchList(text, BATCHSIZE) - else: - format = "text" - tList = [text] - - for index, tItem in enumerate(tList): - # Before sending to translation, if we have a list of items, add the formatting - if isinstance(tItem, list): - payload = {f"Line{i+1}": string for i, string in enumerate(tItem)} - payload = json.dumps(payload, indent=4, ensure_ascii=False) - varResponse = subVars(payload) - subbedT = varResponse[0] - else: - varResponse = subVars(tItem) - subbedT = varResponse[0] - - # Things to Check before starting translation - if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", subbedT): - if PBAR is not None: - PBAR.update(len(tItem)) - continue - - # Create Message - system, user = createContext(fullPromptFlag, subbedT, format) - - # Calculate Estimate - if ESTIMATE: - estimate = countTokens(system, user, history) - totalTokens[0] += estimate[0] - totalTokens[1] += estimate[1] - continue - - # Translating - response = translateText(system, user, history, 0.05, format) - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Check Translation - translatedText = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedText, True) - if extractedTranslations == None or len(tItem) != len(extractedTranslations): - # Mismatch. Try Again - response = translateText(system, user, history, 0.05, format, "gpt-4o") - translatedText = response.choices[0].message.content - totalTokens[0] += response.usage.prompt_tokens - totalTokens[1] += response.usage.completion_tokens - - # Formatting - translatedText = cleanTranslatedText(translatedText, varResponse) - if isinstance(tItem, list): - extractedTranslations = extractTranslation(translatedText, True) - if extractedTranslations == None or len(tItem) != len(extractedTranslations): - mismatch = True # Just here for breakpoint - - # Set if no mismatch - if mismatch == False: - tList[index] = extractedTranslations - history = extractedTranslations[-10:] # Update history if we have a list - else: - history = text[-10:] - mismatch = False - if FILENAME not in MISMATCH: - MISMATCH.append(FILENAME) - - # Update Loading Bar - with LOCK: - if PBAR is not None: - PBAR.update(len(tItem)) - else: - # Ensure we're passing a single string to extractTranslation - tList[index] = translatedText.replace("Placeholder Text", "") - - # Combine if multilist - if isinstance(tList[0], list): - tList = [t for sublist in tList for t in sublist] - - # Return - if format == "json": - return [tList, totalTokens] - else: - return [tList[0], totalTokens] +# Libraries +import json +import os +import re +import textwrap +import threading +import time +import traceback +import tiktoken +import openai +from pathlib import Path +from colorama import Fore +from dotenv import load_dotenv +from retry import retry +from tqdm import tqdm + +# Open AI +load_dotenv() +if os.getenv("api").replace(" ", "") != "": + openai.base_url = os.getenv("api") +openai.organization = os.getenv("org") +openai.api_key = os.getenv("key") + +# Globals +MODEL = os.getenv("model") +TIMEOUT = int(os.getenv("timeout")) +LANGUAGE = os.getenv("language").capitalize() +PROMPT = Path("prompt.txt").read_text(encoding="utf-8") +VOCAB = Path("vocab.txt").read_text(encoding="utf-8") +THREADS = int(os.getenv("threads")) +LOCK = threading.Lock() +WIDTH = int(os.getenv("width")) +LISTWIDTH = int(os.getenv("listWidth")) +NOTEWIDTH = 70 +MAXHISTORY = 10 +ESTIMATE = "" +TOKENS = [0, 0] +NAMESLIST = [] +NAMES = False # Output a list of all the character names found +BRFLAG = False # If the game uses
instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False +PBAR = None +FILENAME = None + +# Full Width +ascii_to_wide = dict((i, chr(i + 0xFEE0)) for i in range(0x21, 0x7F)) +ascii_to_wide.update({0x20: "\u3000", 0x2D: "\u2212"}) # space and minus +wide_to_ascii = dict((i, chr(i - 0xFEE0)) for i in range(0xFF01, 0xFF5F)) +wide_to_ascii.update({0x3000: " ", 0x2212: "-"}) # space and minus + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 40 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleUnity(filename, estimate): + global ESTIMATE, FILENAME + ESTIMATE = estimate + FILENAME = filename + + if ESTIMATE: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + else: + try: + with open("translated/" + filename, "w", encoding="utf8", errors="ignore") as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + outFile.writelines(translatedData[0]) + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + except Exception: + traceback.print_exc() + return "Fail" + + return getResultString(["", TOKENS, None], end - start, "TOTAL") + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="utf8") as readFile: + translatedData = parseUnity(readFile, filename) + + # Delete lines marked for deletion + finalData = [] + for line in translatedData[0]: + if line != "\\d\n": + finalData.append(line) + translatedData[0] = finalData + + return translatedData + + +def parseUnity(readFile, filename): + totalTokens = [0, 0] + + # Read File into data + data = readFile.readlines() + + # Create Progress Bar + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + + try: + result = translateUnity(data, pbar, filename, []) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateUnity(data, pbar, filename, translatedList): + stringList = [] + currentGroup = [] + tokens = [0, 0] + speaker = "" + voice = False + global LOCK, ESTIMATE, PBAR + PBAR = pbar + i = 0 + + # Dialogue + while i < len(data): + # Lines + regex = r".*?=(.*)" + match = re.search(regex, data[i]) + if match != None and match.group(1) != "": + originalString = match.group(1) + # Pass 1 + if translatedList == []: + # Grab Consecutive Strings + jaString = match.group(1) + + # Remove textwrap + jaString = jaString.replace("\n", "") + + # Add String + stringList.append(jaString.strip()) + + # Pass 2 + else: + # Get Text + if translatedList: + # Grab and Pop + translatedText = translatedList[0] + translatedList.pop(0) + + # Set to None if empty list + if len(translatedList) <= 0: + translatedList = None + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + translatedText = translatedText.replace("\n", "\\n") + + # Remove Double Spaces and = + translatedText = translatedText.replace(" ", " ") + translatedText = translatedText.replace("=", "->") + + # Set Data + data[i] = f"{originalString}{originalString}={translatedText}\n" + i += 1 + + # Nothing relevant. Skip Line. + else: + i += 1 + + # EOF + if len(stringList) > 0: + # Set Progress + pbar.total = len(stringList) + pbar.refresh() + + # Translate + response = translateGPT(stringList, "", True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedList = response[0] + + # Set Strings + if len(stringList) == len(translatedList): + translateUnity(data, pbar, filename, translatedList) + + # Mismatch + else: + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + return tokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + if speaker not in str(NAMESLIST): + response = translateGPT( + speaker, + "Reply with the " + LANGUAGE + " translation of the NPC name.", + True, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + + # Retry if name doesn't translate for some reason + if re.search(r"([a-zA-Z??])", response[0]) == None: + response = translateGPT( + speaker, + "Reply with the " + LANGUAGE + " translation of the NPC name.", + False, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + + speakerList = [speaker, response[0]] + NAMESLIST.append(speakerList) + return response + + # Find Speaker + else: + for i in range(len(NAMESLIST)): + if speaker == NAMESLIST[i][0]: + return [NAMESLIST[i][1], [0, 0]] + + return [speaker, [0, 0]] + + +def subVars(jaString): + jaString = jaString.replace("\u3000", " ") + + # Formatting + count = 0 + codeList = re.findall(r"[\\]+[\w]+\[[a-zA-Z0-9\\\[\]\_,\s-]+\]", jaString) + codeList = set(codeList) + if len(codeList) != 0: + for var in codeList: + jaString = jaString.replace(var, "[FCode_" + str(count) + "]") + count += 1 + + # Put all lists in list and return + return [jaString, codeList] + + +def resubVars(translatedText, codeList): + # Fix Spacing and ChatGPT Nonsense + matchList = re.findall(r"\[\s?.+?\s?\]", translatedText) + if len(matchList) > 0: + for match in matchList: + text = match.strip() + translatedText = translatedText.replace(match, text) + + # Formatting + count = 0 + if len(codeList) != 0: + for var in codeList: + translatedText = translatedText.replace("[FCode_" + str(count) + "]", var) + count += 1 + + return translatedText + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT, format): + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + if format == "json": + user = f"```json\n{subbedT}\n```" + else: + user = subbedT + return system, user + + +def translateText(system, user, history, penalty, format, model=MODEL): + # Prompt + msg = [{"role": "system", "content": system}] + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Response Format + if format == "json": + responseFormat = {"type": "json_object"} + else: + responseFormat = {"type": "text"} + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=model, + response_format=responseFormat, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "「": '\\"', + "」": '\\"', + "- ": "-", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + translatedText = resubVars(translatedText, varResponse[1]) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + try: + line_dict = json.loads(translatedTextList) + # If it's a batch (i.e., list), extract with tags; otherwise, return the single item. + string_list = list(line_dict.values()) + if is_list: + return string_list + else: + return string_list[0] + + except Exception as e: + print(f"extractTranslation Error: {e}") + return None + + +def countTokens(system, user, history): + inputTotalTokens = 0 + outputTotalTokens = 0 + enc = tiktoken.encoding_for_model("gpt-4") + + # Input + if isinstance(history, list): + for line in history: + inputTotalTokens += len(enc.encode(line)) + else: + inputTotalTokens += len(enc.encode(history)) + inputTotalTokens += len(enc.encode(system)) + inputTotalTokens += len(enc.encode(user)) + + # Output + outputTotalTokens += round(len(enc.encode(user)) * 3) + + return [inputTotalTokens, outputTotalTokens] + + +@retry(exceptions=Exception, tries=5, delay=5) +def translateGPT(text, history, fullPromptFlag): + global PBAR, MISMATCH, FILENAME + + mismatch = False + totalTokens = [0, 0] + if isinstance(text, list): + format = "json" + tList = batchList(text, BATCHSIZE) + else: + format = "text" + tList = [text] + + for index, tItem in enumerate(tList): + # Before sending to translation, if we have a list of items, add the formatting + if isinstance(tItem, list): + payload = {f"Line{i+1}": string for i, string in enumerate(tItem)} + payload = json.dumps(payload, indent=4, ensure_ascii=False) + varResponse = subVars(payload) + subbedT = varResponse[0] + else: + varResponse = subVars(tItem) + subbedT = varResponse[0] + + # Things to Check before starting translation + if not re.search(r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", subbedT): + if PBAR is not None: + PBAR.update(len(tItem)) + continue + + # Create Message + system, user = createContext(fullPromptFlag, subbedT, format) + + # Calculate Estimate + if ESTIMATE: + estimate = countTokens(system, user, history) + totalTokens[0] += estimate[0] + totalTokens[1] += estimate[1] + continue + + # Translating + response = translateText(system, user, history, 0.05, format) + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Check Translation + translatedText = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedText, True) + if extractedTranslations == None or len(tItem) != len(extractedTranslations): + # Mismatch. Try Again + response = translateText(system, user, history, 0.05, format, "gpt-4o") + translatedText = response.choices[0].message.content + totalTokens[0] += response.usage.prompt_tokens + totalTokens[1] += response.usage.completion_tokens + + # Formatting + translatedText = cleanTranslatedText(translatedText, varResponse) + if isinstance(tItem, list): + extractedTranslations = extractTranslation(translatedText, True) + if extractedTranslations == None or len(tItem) != len(extractedTranslations): + mismatch = True # Just here for breakpoint + + # Set if no mismatch + if mismatch == False: + tList[index] = extractedTranslations + history = extractedTranslations[-10:] # Update history if we have a list + else: + history = text[-10:] + mismatch = False + if FILENAME not in MISMATCH: + MISMATCH.append(FILENAME) + + # Update Loading Bar + with LOCK: + if PBAR is not None: + PBAR.update(len(tItem)) + else: + # Ensure we're passing a single string to extractTranslation + tList[index] = translatedText.replace("Placeholder Text", "") + + # Combine if multilist + if isinstance(tList[0], list): + tList = [t for sublist in tList for t in sublist] + + # Return + if format == "json": + return [tList, totalTokens] + else: + return [tList[0], totalTokens] diff --git a/modules/wolf2.py b/modules/wolf2.py index 76b614e..2859761 100644 --- a/modules/wolf2.py +++ b/modules/wolf2.py @@ -1,579 +1,580 @@ -# Libraries -import json -import os -import re -import textwrap -import threading -import time -import traceback -import tiktoken -import openai -from pathlib import Path -from colorama import Fore -from dotenv import load_dotenv -from retry import retry -from tqdm import tqdm - -# Open AI -load_dotenv() -if os.getenv("api").replace(" ", "") != "": - openai.base_url = os.getenv("api") -openai.organization = os.getenv("org") -openai.api_key = os.getenv("key") - -# Globals -MODEL = os.getenv("model") -TIMEOUT = int(os.getenv("timeout")) -LANGUAGE = os.getenv("language").capitalize() -PROMPT = Path("prompt.txt").read_text(encoding="utf-8") -VOCAB = Path("vocab.txt").read_text(encoding="utf-8") -THREADS = int(os.getenv("threads")) -LOCK = threading.Lock() -WIDTH = int(os.getenv("width")) -LISTWIDTH = int(os.getenv("listWidth")) -NOTEWIDTH = 70 -MAXHISTORY = 10 -ESTIMATE = "" -TOKENS = [0, 0] -NAMESLIST = [] -NAMES = False # Output a list of all the character names found -BRFLAG = False # If the game uses
instead -FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 -LEAVE = False - -# Pricing - Depends on the model https://openai.com/pricing -# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request -# If you are getting a MISMATCH LENGTH error, lower the batch size. -if "gpt-3.5" in MODEL: - INPUTAPICOST = 0.002 - OUTPUTAPICOST = 0.002 - BATCHSIZE = 10 -elif "gpt-4" in MODEL: - INPUTAPICOST = 0.0025 - OUTPUTAPICOST = 0.01 - BATCHSIZE = 40 -else: - INPUTAPICOST = float(os.getenv("input_cost")) - OUTPUTAPICOST = float(os.getenv("output_cost")) - BATCHSIZE = int(os.getenv("batchsize")) - FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) - -def handleWOLF2(filename, estimate): - global ESTIMATE - ESTIMATE = estimate - - if ESTIMATE: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - else: - try: - with open("translated/" + filename, "w", encoding="shift_jis", errors="ignore") as outFile: - start = time.time() - translatedData = openFiles(filename) - - # Print Result - end = time.time() - outFile.writelines(translatedData[0]) - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - except Exception: - traceback.print_exc() - return "Fail" - - return getResultString(["", TOKENS, None], end - start, "TOTAL") - - -def getResultString(translatedData, translationTime, filename): - # File Print String - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) - + "]" - ) - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - - if translatedData[2] == None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def openFiles(filename): - with open("files/" + filename, "r", encoding="shift_jis") as readFile: - translatedData = parseWOLF(readFile, filename) - - # Delete lines marked for deletion - finalData = [] - for line in translatedData[0]: - if line != "\\d\n": - finalData.append(line) - translatedData[0] = finalData - - return translatedData - - -def parseWOLF(readFile, filename): - totalTokens = [0, 0] - - # Read File into data - data = readFile.readlines() - - # Create Progress Bar - with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: - pbar.desc = filename - - try: - result = translateWOLF(data, [], pbar, filename) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - return [data, totalTokens, None] - - -def translateWOLF(data, translatedList, pbar, filename): - stringList = [] - currentGroup = [] - tokens = [0, 0] - speaker = "" - global LOCK, ESTIMATE, PBAR - PBAR = pbar - i = 0 - - while i < len(data): - # Speaker - matchList = re.findall(r"(.*):", data[i]) - if len(matchList) != 0: - response = getSpeaker(matchList[0]) - speaker = response[0] - tokens[0] += response[1][0] - tokens[1] += response[1][1] - data[i] = f"{speaker}:\n" - i += 1 - else: - speaker = "" - - # Options - if "//選択肢" in data[i]: - i += 1 - choiceList = [] - initialIndex = i - while "//" in data[i] and "の場合" not in data[i]: - choiceList.append(re.search(r"\/\/(.*)", data[i]).group(1)) - i += 1 - - # Translate - response = translateGPT(choiceList, "This will be a dialogue option", True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - choiceListTL = response[0] - - # Set Data - if len(choiceList) == len(choiceListTL): - # Set Data - i = initialIndex - while "//" in data[i] and "の場合" not in data[i]: - choiceListTL[0] = choiceListTL[0].replace(", ", "、") - data[i] = f"//{choiceListTL[0]}\n" - choiceListTL.pop(0) - i += 1 - - # Mismatch - else: - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - - # Lines - if r"/" not in data[i] and "@" not in data[i] and data[i] != "\n": - # Pass 1 - if translatedList == []: - # Grab Consecutive Strings - currentGroup.append(data[i]) - i += 1 - while i < len(data) and r"/" not in data[i] and "@" not in data[i] and data[i] != "\n": - currentGroup.append(data[i]) - i += 1 - - # Join up 401 groups for better translation. - if len(currentGroup) > 0: - jaString = "".join(currentGroup) - currentGroup = [] - - # Remove any textwrap - jaString = jaString.replace("\n", " ") - - # Add Speaker (If there is one) - if speaker != "": - jaString = f"{speaker}: {jaString}" - - # Add String - stringList.append(jaString) - i += 1 - - # Pass 2 - else: - # Insert Strings - while i < len(data) and r"/" not in data[i] and "@" not in data[i] and data[i] != "\n": - data.pop(i) - - # Get Text - translatedText = translatedList[0] - translatedList.pop(0) - - if len(translatedList) <= 0: - translatedList = None - - # Remove added speaker - # translatedText = re.sub(r"^.+?:\s", "", translatedText) - - # Textwrap - translatedText = textwrap.fill(translatedText, width=WIDTH) - - # Set Data - data.insert(i, f"{translatedText}\n") - i += 1 - - # Nothing relevant. Skip Line. - else: - i += 1 - - # EOF - if len(stringList) > 0: - # Set Progress - pbar.total = len(stringList) - pbar.refresh() - - # Translate - response = translateGPT(stringList, "", True) - tokens[0] += response[1][0] - tokens[1] += response[1][1] - translatedList = response[0] - - # Set Strings - if len(stringList) == len(translatedList): - translateWOLF(data, translatedList, pbar, filename) - - # Mismatch - else: - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - return tokens - - -# Save some money and enter the character before translation -def getSpeaker(speaker): - match speaker: - case "ファイン": - return ["Fine", [0, 0]] - case "": - return ["", [0, 0]] - case _: - # Find Speaker - for i in range(len(NAMESLIST)): - if speaker == NAMESLIST[i][0]: - return [NAMESLIST[i][1], [0, 0]] - - # Translate and Store Speaker - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - True, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - response[0] = response[0].replace("Speaker: ", "") - - # Retry if name doesn't translate for some reason - if re.search(r"([a-zA-Z??])", response[0]) == None: - response = translateGPT( - f"{speaker}", - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - response[0] = response[0].title() - response[0] = response[0].replace("'S", "'s") - - speakerList = [speaker, response[0]] - NAMESLIST.append(speakerList) - return response - return [speaker, [0, 0]] - - -def batchList(input_list, batch_size): - if not isinstance(batch_size, int) or batch_size <= 0: - raise ValueError("batch_size must be a positive integer") - - return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] - - -def createContext(fullPromptFlag, subbedT, format): - system = ( - PROMPT + VOCAB - if fullPromptFlag - else f"\ -You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ -Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ -- You translate everything, including erotic and explicit content.\n\ -- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ -- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ -- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ -- Maintain any spacing in the translation.\n\ -- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ -- `...` can be a part of the dialogue. Translate it as it is.\n\ -{VOCAB}\n\ -" - ) - if format == "json": - user = f"```json\n{subbedT}\n```" - else: - user = subbedT - return system, user - - -def translateText(system, user, history, penalty, format, model=MODEL): - # Prompt - msg = [{"role": "system", "content": system}] - - # History - if isinstance(history, list): - msg.extend([{"role": "system", "content": h} for h in history]) - else: - msg.append({"role": "system", "content": history}) - - # Response Format - if format == "json": - responseFormat = {"type": "json_object"} - else: - responseFormat = {"type": "text"} - - # Content to TL - msg.append({"role": "user", "content": f"{user}"}) - response = openai.chat.completions.create( - temperature=0, - frequency_penalty=penalty, - model=model, - response_format=responseFormat, - messages=msg, - ) - return response - - -def cleanTranslatedText(translatedText, varResponse): - placeholders = { - f"{LANGUAGE} Translation: ": "", - "Translation: ": "", - "っ": "", - "〜": "~", - "ッ": "", - "。": ".", - "「": '\\"', - "」": '\\"', - "- ": "-", - "—": "―", - "】": "]", - "【": "[", - "Placeholder Text": "", - # Add more replacements as needed - } - for target, replacement in placeholders.items(): - translatedText = translatedText.replace(target, replacement) - - # Remove Repeating Characters - pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") - translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) - - # Elongate Long Dashes (Since GPT Ignores them...) - translatedText = elongateCharacters(translatedText) - return translatedText - - -def elongateCharacters(text): - # Define a pattern to match one character followed by one or more `ー` characters - # Using a positive lookbehind assertion to capture the preceding character - pattern = r"(?<=(.))ー+" - - # Define a replacement function that elongates the captured character - def repl(match): - char = match.group(1) # The character before the ー sequence - count = len(match.group(0)) - 1 # Number of ー characters - return char * count # Replace ー sequence with the character repeated - - # Use re.sub() to replace the pattern in the text - return re.sub(pattern, repl, text) - - -def extractTranslation(translatedTextList, is_list): - try: - translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) - translatedTextList = re.sub(r"(? instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 40 +else: + INPUTAPICOST = float(os.getenv("input_cost")) + OUTPUTAPICOST = float(os.getenv("output_cost")) + BATCHSIZE = int(os.getenv("batchsize")) + FREQUENCY_PENALTY = float(os.getenv("frequency_penalty")) + + +def handleWOLF2(filename, estimate): + global ESTIMATE + ESTIMATE = estimate + + if ESTIMATE: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + else: + return totalString + + else: + try: + with open("translated/" + filename, "w", encoding="shift_jis", errors="ignore") as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + outFile.writelines(translatedData[0]) + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + except Exception: + traceback.print_exc() + return "Fail" + + return getResultString(["", TOKENS, None], end - start, "TOTAL") + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format((translatedData[1][0] * 0.001 * INPUTAPICOST) + (translatedData[1][1] * 0.001 * OUTPUTAPICOST)) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="shift_jis") as readFile: + translatedData = parseWOLF(readFile, filename) + + # Delete lines marked for deletion + finalData = [] + for line in translatedData[0]: + if line != "\\d\n": + finalData.append(line) + translatedData[0] = finalData + + return translatedData + + +def parseWOLF(readFile, filename): + totalTokens = [0, 0] + + # Read File into data + data = readFile.readlines() + + # Create Progress Bar + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + + try: + result = translateWOLF(data, [], pbar, filename) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateWOLF(data, translatedList, pbar, filename): + stringList = [] + currentGroup = [] + tokens = [0, 0] + speaker = "" + global LOCK, ESTIMATE, PBAR + PBAR = pbar + i = 0 + + while i < len(data): + # Speaker + matchList = re.findall(r"(.*):", data[i]) + if len(matchList) != 0: + response = getSpeaker(matchList[0]) + speaker = response[0] + tokens[0] += response[1][0] + tokens[1] += response[1][1] + data[i] = f"{speaker}:\n" + i += 1 + else: + speaker = "" + + # Options + if "//選択肢" in data[i]: + i += 1 + choiceList = [] + initialIndex = i + while "//" in data[i] and "の場合" not in data[i]: + choiceList.append(re.search(r"\/\/(.*)", data[i]).group(1)) + i += 1 + + # Translate + response = translateGPT(choiceList, "This will be a dialogue option", True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + choiceListTL = response[0] + + # Set Data + if len(choiceList) == len(choiceListTL): + # Set Data + i = initialIndex + while "//" in data[i] and "の場合" not in data[i]: + choiceListTL[0] = choiceListTL[0].replace(", ", "、") + data[i] = f"//{choiceListTL[0]}\n" + choiceListTL.pop(0) + i += 1 + + # Mismatch + else: + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + + # Lines + if r"/" not in data[i] and "@" not in data[i] and data[i] != "\n": + # Pass 1 + if translatedList == []: + # Grab Consecutive Strings + currentGroup.append(data[i]) + i += 1 + while i < len(data) and r"/" not in data[i] and "@" not in data[i] and data[i] != "\n": + currentGroup.append(data[i]) + i += 1 + + # Join up 401 groups for better translation. + if len(currentGroup) > 0: + jaString = "".join(currentGroup) + currentGroup = [] + + # Remove any textwrap + jaString = jaString.replace("\n", " ") + + # Add Speaker (If there is one) + if speaker != "": + jaString = f"{speaker}: {jaString}" + + # Add String + stringList.append(jaString) + i += 1 + + # Pass 2 + else: + # Insert Strings + while i < len(data) and r"/" not in data[i] and "@" not in data[i] and data[i] != "\n": + data.pop(i) + + # Get Text + translatedText = translatedList[0] + translatedList.pop(0) + + if len(translatedList) <= 0: + translatedList = None + + # Remove added speaker + # translatedText = re.sub(r"^.+?:\s", "", translatedText) + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + + # Set Data + data.insert(i, f"{translatedText}\n") + i += 1 + + # Nothing relevant. Skip Line. + else: + i += 1 + + # EOF + if len(stringList) > 0: + # Set Progress + pbar.total = len(stringList) + pbar.refresh() + + # Translate + response = translateGPT(stringList, "", True) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedList = response[0] + + # Set Strings + if len(stringList) == len(translatedList): + translateWOLF(data, translatedList, pbar, filename) + + # Mismatch + else: + with LOCK: + if filename not in MISMATCH: + MISMATCH.append(filename) + return tokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + match speaker: + case "ファイン": + return ["Fine", [0, 0]] + case "": + return ["", [0, 0]] + case _: + # Find Speaker + for i in range(len(NAMESLIST)): + if speaker == NAMESLIST[i][0]: + return [NAMESLIST[i][1], [0, 0]] + + # Translate and Store Speaker + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + True, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + response[0] = response[0].replace("Speaker: ", "") + + # Retry if name doesn't translate for some reason + if re.search(r"([a-zA-Z??])", response[0]) == None: + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + False, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + + speakerList = [speaker, response[0]] + NAMESLIST.append(speakerList) + return response + return [speaker, [0, 0]] + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size)] + + +def createContext(fullPromptFlag, subbedT, format): + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + if format == "json": + user = f"```json\n{subbedT}\n```" + else: + user = subbedT + return system, user + + +def translateText(system, user, history, penalty, format, model=MODEL): + # Prompt + msg = [{"role": "system", "content": system}] + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Response Format + if format == "json": + responseFormat = {"type": "json_object"} + else: + responseFormat = {"type": "text"} + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=model, + response_format=responseFormat, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "「": '\\"', + "」": '\\"', + "- ": "-", + "—": "―", + "】": "]", + "【": "[", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Remove Repeating Characters + pattern = re.compile(r"(.)\s*\1(?:\s*\1){" + str(20 - 1) + r",}") + translatedText = pattern.sub(lambda match: match.group(0).replace(" ", "")[:20], translatedText) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + try: + translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) + translatedTextList = re.sub(r"(?