🎮 Z.A.T.O. Visual Novel Dialogue Extractor

视觉小说台词自动提取助手 (Python Script)

这是一个专为视觉小说 《Z.A.T.O. // I Love the World and Everything in It》 编写的 Python 辅助脚本。它可以利用 Ren’Py 引擎的剪贴板辅助功能,自动将游戏台词提取并保存为干净的 Markdown (.md) 格式,方便翻译、归档或语言学习。

核心特性:智能降噪、自动去重、LOG 熔断保护、热键控制。


✨ 功能特点 (Features)

  • 🛡️ 智能降噪 (Smart Noise Cancellation)
    • 自动识别并过滤游戏 UI 杂音(如 Settings, Save Slot, Auto forward 等)。
    • 基于正则表达式 (Regex) 过滤动态系统信息(如页码、音量条)。
    • 内置“必杀短语”库,精准拦截系统弹窗。
  • 🔄 双重去重机制 (Dual-Layer Dedup)
    • 物理层:监控剪贴板变动,防止 CPU 空转。
    • 逻辑层:防止因中间夹杂噪音(如鼠标滑过按钮)导致同一句台词被重复记录。
  • 🔥 LOG 熔断保护 (Log Meltdown Protection)
    • 自动检测超长文本(默认 > 350字符)。防止打开游戏历史记录 (LOG) 时,大量重复文本刷屏。
  • ⌨️ 全局热键控制
    • F10:一键 暂停/继续 录制(方便切出游戏查词或复制文本)。
    • Shift + Esc:安全退出脚本(避免与游戏内的 Esc 菜单键冲突)。
  • 📝 格式化输出
    • 自动处理 Windows 换行符 (\r\n)。
    • 输出带时间戳的 Markdown 引用格式,清晰易读。

🛠️ 安装与依赖 (Installation)

1. 环境要求

  • Python 3.x
  • Windows 系统 (推荐,因使用了 pyperclip 对 Windows 换行符的特定优化)

2.🚀 使用指南 (Usage):

安装依赖库:

在终端中运行以下命令:

pip install pyperclip keyboard

启动脚本: 在终端中运行脚本:

python zato_extractor.py

看到终端提示 🟢 录制中 即表示准备就绪。

进入游戏: 打开《Z.A.T.O.》并进入游戏画面。

开启引擎辅助: 在游戏内按下 Shift + C这是 Ren’Py 引擎的快捷键,开启后游戏会自动将当前台词复制到剪贴板。

开始游玩: 正常点击鼠标游玩即可。脚本会在后台自动将台词写入 zato_script.md 文件。

控制录制

  • 需要暂停录制(例如要切出来复制东西)时,按 F10
  • 结束游玩时,按 Shift + Esc 退出脚本。

⚙️ 配置 (Configuration)

你可以直接打开 .py 文件修改顶部的配置区域:

# 输出文件名
OUTPUT_FILE = "zato_script.md"

# 扫描频率 (秒)
CHECK_INTERVAL = 0.2

# 热键设置
TOGGLE_KEY = 'f10' # 暂停/继续
EXIT_KEY = 'shift+esc' # 退出脚本

# LOG 熔断阈值 (超过此长度的文本将被丢弃)
MAX_CHAR_LIMIT = 350

❓ 常见问题 (FAQ)

Q: 为什么脚本没有记录台词? A: 请确保游戏内已按下 Shift + C(通常会听到提示音或看到左上角提示 “Clipboard Voice Enabled”)。

Q: 为什么我的终端里出现了一个白色的输入框? A: 这是 Windows CMD 窗口的特性。如果你按下了某些快捷键(如 F9),CMD 会暂停脚本。请按 Esc 关闭该框,建议使用 F10 作为控制键。

Q: 这个脚本支持其他游戏吗? A: 理论上支持所有基于 Ren’Py 引擎开发的游戏。但不同游戏的 UI 噪音词库不同,你可能需要修改脚本中的 STRICT_BLOCKLIST(黑名单)来适配其他游戏。


📜 开源协议 (License)

MIT License. 仅供学习交流使用。 游戏文本版权归原作者 Nopanamaman (Ferry) 所有。

源代码:

import pyperclip
import time
import os
import re
import keyboard
from datetime import datetime

# --- ⚙️ 配置区域 ---
OUTPUT_FILE = "zato_script.md"
CHECK_INTERVAL = 0.2
TOGGLE_KEY = 'f10'
EXIT_KEY = 'shift+esc'

# 📏 [新增] 字数熔断阈值
# 正常游戏台词一屏很难超过 300 字符 (英文)。
# 如果剪贴板内容超过这个长度,视为 LOG 历史记录堆积,直接丢弃。
MAX_CHAR_LIMIT = 350

# 💀 必杀短语 (Death Phrases)
DEATH_PHRASES = [
"lose unsaved progress",
"return to the main menu",
"Are you sure you want to quit",
"Z.A.T.O. // I Love the World",
"display Window",
"Previous file page", "Next file page",
"File page quick", "File page auto"
]

# 🚫 绝对黑名单
STRICT_BLOCKLIST = [
"YES", "NO", "RETURN", "BACK", "HISTORY", "SKIP", "AUTO",
"PREFS", "SAVE", "LOAD", "Q.SAVE", "Q.LOAD", "TITLE", "LOG",
"QUIT", "CREDITS", "CONTROLS", "SETTINGS",
"Settings", "Load", "Save", "Return", "Back", "History",
"Skip", "Auto", "Q.Save", "Q.Load", "Prefs", "Credits",
"Display", "Window", "Fullscreen", "Language",
"Text Speed", "Music Volume", "Sound Volume", "Voice Volume",
"Self-Voicing", "Clipboard Voice",
"Clipboard Voice Enable", "Clipboard Voice Disable",
"Auto forward", "Quick save", "Quick save.",
"mute all", "skip unseen text", "skip transitions", "skip after choices"
]

# --- 🛠️ 智能过滤函数 ---

def is_noise(text):
# 1. 预处理
clean = text.replace('\r\n', '\n').strip()

# 2. [新增] 📏 字数熔断检查 (最有效的 LOG 杀手)
# 如果文本太长,直接判定为噪音 (LOG 堆积)
if len(clean) > MAX_CHAR_LIMIT:
return True

# 3. [新增] 🏷️ LOG 尾缀检查
# 你的日志里出现了 "...morning sky.: LOG"
if clean.endswith(": LOG") or clean.endswith(": HISTORY"):
return True

# 4. 标点护盾 (包含引号放行,但必须在长度限制之内)
if '"' in clean or '“' in clean or '”' in clean:
return False

# 5. 基础长度过滤 (太短也不行)
if not clean or len(clean) < 2:
return True

# 6. 必杀短语
for phrase in DEATH_PHRASES:
if phrase in clean:
return True

# 7. 精准黑名单
if clean in STRICT_BLOCKLIST:
return True

# 8. 正则特征查杀
if re.search(r'(file page|slot) \d+', clean, re.IGNORECASE): return True
if re.search(r'(selected|bar)$', clean, re.IGNORECASE): return True
if re.match(r'^(Enter|Space|Arrow|Escape|Ctrl|Tab|Page|Shift\+\w|H|S|V):', clean): return True
if re.search(r'(MUSIC BY|THEME:|VOICE:|TESTING,|PROOFREADING:)', clean): return True

# 全大写检查 (保留带情绪标点的短语)
if clean.isupper() and len(clean) < 15:
if '!' in clean or '?' in clean: return False
return True

if re.match(r'^[-=_.]{3,}$', clean): return True

return False

def contains_chinese(text):
return bool(re.search(r'[\u4e00-\u9fa5]', text))

# --- 🚀 主程序 ---

def main():
print(f"🕵️‍♂️ 翻译助理 v4.8 (LOG 熔断版) 启动!")
print(f"📏 已设置长文本熔断阈值: {MAX_CHAR_LIMIT} 字符")
print(f"🎮 退出: [Shift+Esc] | 开关: [{TOGGLE_KEY.upper()}]")
print("-" * 40)

is_recording = True
last_raw_clipboard = ""
last_saved_text = ""

pyperclip.copy("")

try:
while True:
if keyboard.is_pressed(EXIT_KEY):
print("\n👋 用户主动退出")
break
if keyboard.is_pressed(TOGGLE_KEY):
is_recording = not is_recording
status = "🟢 录制中" if is_recording else "🔴 已暂停"
print(f"\n>> {status} <<\n")
time.sleep(0.5)

if not is_recording:
time.sleep(CHECK_INTERVAL)
continue

try:
current_raw = pyperclip.paste()
except:
time.sleep(CHECK_INTERVAL)
continue

# 物理去重
if current_raw == last_raw_clipboard:
time.sleep(CHECK_INTERVAL)
continue

last_raw_clipboard = current_raw

# 噪音过滤
if contains_chinese(current_raw):
continue
if is_noise(current_raw):
# print(f"🗑️ [噪音] {current_raw[:15]}...")
continue

# 逻辑去重
clean_write = current_raw.strip().replace('\r\n', '\n')
if clean_write == last_saved_text:
continue

# ✅ 写入
timestamp = datetime.now().strftime("%H:%M")
with open(OUTPUT_FILE, "a", encoding="utf-8") as f:
f.write(f"> [{timestamp}] {clean_write}\n\n")

print(f"✨ [台词] {clean_write[:40]}...")
last_saved_text = clean_write

time.sleep(CHECK_INTERVAL)

except KeyboardInterrupt:
print("\n🛑 脚本停止")

if __name__ == "__main__":
main()