From 95bf4c0b841e87a5ad2f9e2b2ebb1d2bcaeca7aa Mon Sep 17 00:00:00 2001 From: dazedanon Date: Mon, 15 Sep 2025 14:30:08 -0500 Subject: [PATCH] Some kirikiri updates --- modules/kirikiri.py | 172 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 147 insertions(+), 25 deletions(-) diff --git a/modules/kirikiri.py b/modules/kirikiri.py index f9bb038..3b237e1 100644 --- a/modules/kirikiri.py +++ b/modules/kirikiri.py @@ -105,25 +105,34 @@ def handleKirikiri(filename, estimate): return totalString else: + # We no longer keep the destination file open during translation because + # incremental progress saves (save_progress_lines) need to atomically + # replace the file on Windows. Holding an open handle prevents os.replace + # from succeeding (WinError 5 Access is denied). try: - with open("translated/" + filename, "w", encoding="cp932", errors="ignore") as outFile: - start = time.time() - translatedData = openFiles(filename) + start = time.time() + translatedData = openFiles(filename) + end = time.time() - # 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] + # Final write safeguard: if for some reason the progress file was + # never written (e.g. no translatable lines triggered saves), write it now. + try: + if translatedData[0]: + os.makedirs("translated", exist_ok=True) + final_path = os.path.join("translated", filename) + # Write directly (small risk window acceptable on final flush) + with open(final_path, "w", encoding="cp932", errors="ignore", newline="\n") as f: + f.writelines(translatedData[0]) + except Exception: + traceback.print_exc() + + 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}") + # Don't blindly remove the file; it may contain partial progress. return "Fail" return getResultString(["", TOKENS, None], end - start, "TOTAL") @@ -183,24 +192,57 @@ def parseKiriKiri(readFile, filename): def save_progress_lines(lines, filename, encoding="cp932"): - """Atomically save current line-based translation progress.""" - try: - if ESTIMATE: - return - os.makedirs("translated", exist_ok=True) - tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated") + """Atomically (with retries) save current line-based translation progress. + + Rationale: + Windows raises PermissionError if the destination file is open elsewhere. + We avoid holding an open handle outside this function and add a small + exponential backoff retry loop to handle transient locks (e.g. AV scanners). + """ + if ESTIMATE: + return + + max_attempts = 5 + backoff = 0.05 # seconds + tmp_fd = None + tmp_path = None + + for attempt in range(1, max_attempts + 1): try: + os.makedirs("translated", exist_ok=True) + + # Create temp file every attempt (prior one cleaned in finally). + tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated") with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file: tmp_file.writelines(lines) - os.replace(tmp_path, os.path.join("translated", filename)) + tmp_file.flush() + os.fsync(tmp_file.fileno()) + + dest_path = os.path.join("translated", filename) + try: + os.replace(tmp_path, dest_path) + except PermissionError as e: + # Retry on Windows-specific sharing violation + if attempt < max_attempts: + time.sleep(backoff) + backoff *= 2 + continue + else: + raise e + # Success, break loop + break + except Exception: + if attempt == max_attempts: + traceback.print_exc() finally: - if os.path.exists(tmp_path): + # Ensure temp file removed if it still exists + if tmp_path and os.path.exists(tmp_path): try: os.remove(tmp_path) except OSError: pass - except Exception: - traceback.print_exc() + tmp_fd = None + tmp_path = None def translateKiriKiri(data, pbar, filename, jobList): @@ -223,6 +265,7 @@ def translateKiriKiri(data, pbar, filename, jobList): dialogueRegex = r"^\[text\](.*).*\[KeyWait\]|\[\w+\](.*)\[\/\w+\].*\[KeyWait\]" furiganaRegex = r'(\[eruby\sstr="(.*?)"\stext.*?\])' choicesRegex = r"^\s*\[button\d\sclickse=sys_decide.*text='(.*?)'.*" + taggedDialogueRegex = r"^\[(?P[^\s\]/]+)(?:\s[^\]]*)?\](?P.*?)\[/\1\]" while i < len(data): speaker = "" @@ -259,6 +302,85 @@ def translateKiriKiri(data, pbar, filename, jobList): data[i] = data[i].replace(jaString, translatedText) save_progress_lines(data, filename) + # Tagged dialogue lines e.g., [思考 storage="..."]text[/思考] + tagged = re.match(taggedDialogueRegex, data[i]) + if tagged and DIALOGUE: + tag_name = tagged.group('tag') + jaString = tagged.group('text') + + # Pass 1: enqueue with speaker from closing tag + if not setData: + # Remove inline wrapping + jaString_clean = jaString.replace("[r]", " ") + # Remove furigana + matchList = re.findall(furiganaRegex, jaString_clean) + if matchList: + for fm in matchList: + jaString_clean = jaString_clean.replace(fm[0], fm[1]) + + # Resolve speaker via getSpeaker + resolved = getSpeaker(tag_name) + tag_speaker = resolved[0] + tokens[0] += resolved[1][0] + tokens[1] += resolved[1][1] + + if tag_speaker: + stringList.append(f"[{tag_speaker}]: {jaString_clean.strip()}") + else: + stringList.append(jaString_clean.strip()) + + # Pass 2: apply translated text back between tags + else: + if len(stringList) > 0: + translatedText = stringList[0] + stringList.pop(0) + # Remove Speaker label if present + translatedText = re.sub(r"\[.*?\]:\s", "", translatedText) + # Wrap and convert newlines to [r] + translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) + translatedText = translatedText.replace("\n", "[r]") + # Replace quotes as per convention + data[i] = data[i].replace("'", '"') + translatedText = translatedText.replace('"', "'") + # Replace only inner content + data[i] = data[i].replace(jaString, translatedText) + save_progress_lines(data, filename) + + # Simple narrative line handling: translate each whitespace-led, non-tag, non-command line independently. + # This avoids reflowing or merging blocks, preventing misplaced text. + if not re.match(r"^\[", data[i]) and not data[i].lstrip().startswith("@"): + if re.match(r"^[ \t\u3000]+", data[i]): + # Skip standalone markers like [▼] + if data[i].strip() == "[▼]": + pass + else: + # Pass 1 + if not setData: + line_content = data[i].rstrip("\n") + # Remove inline wrapping markers and glyph markers + line_content = line_content.replace("[r]", " ") + line_content = re.sub(r"\[▼\]", "", line_content) + # Remove furigana blocks + matchList = re.findall(furiganaRegex, line_content) + if matchList: + for fm in matchList: + line_content = line_content.replace(fm[0], fm[1]) + cleaned = line_content.strip() + if cleaned: + stringList.append(cleaned) + # Pass 2 + else: + if len(stringList) > 0: + translatedText = stringList[0] + stringList.pop(0) + translatedText = re.sub(r"\[.*?\]:\s", "", translatedText) + translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) + translatedText = translatedText.replace("\n", "[r]") + indent_match = re.match(r"^([ \t\u3000]+)", data[i]) + indent = indent_match.group(1) if indent_match else "" + data[i] = f"{indent}{translatedText}\n" + save_progress_lines(data, filename) + # Dialogue match = re.search(dialogueRegex, data[i]) if match and DIALOGUE: