画面上の英語/日本語をリアルタイム翻訳するWindowsアプリを作った(exe配布)ゲーム同時翻訳くん(仮)

概要

Windowsのデスクトップを範囲指定し、リアルタイム翻訳するアプリです。
作成を始めたばかりのベータ版状態で、OCR、翻訳ともにいまいちです。
精度が発展途上のため、デザインフォントやドットの粗いフォントのスキャン品質が良くないですが、内よりまり、英語でしかゲームが出ていない時などのプレイを助けてくれるかと思います。

操作動画

アプリ構成

Pythonで作っています。
一部OCRはGoogleのTesseract OCRを別にインストールして使います。

■ Tesseract OCR について
本ツールで Tesseract OCR モードを使用する場合は、
別途 Tesseract OCR をインストールしてください。公式サイト:
https://github.com/tesseract-ocr/tesseract
インストール後、アプリ内の
「tesseract.exe パス」に実行ファイルの場所を指定してください。

ライブラリ

# requirements.txt (v0.5.3)

mss>=9.0.1
pytesseract>=0.3.10
Pillow>=10.0.0
PySide6>=6.6.0
deep-translator>=1.11.4

# EasyOCRを使う場合(任意)
easyocr>=1.7.1
torch>=2.0.0
torchvision>=0.15.0

# EasyOCR内部で使用(環境によっては依存で入るが明示)
numpy>=1.26.0

コード

import os
import sys
from dataclasses import dataclass
from datetime import datetime

import mss
import pytesseract
from PIL import Image, ImageOps

from deep_translator import GoogleTranslator
from PySide6 import QtCore, QtGui, QtWidgets


# ---- DPI Aware(ある程度効くが、座標変換で吸収)----
def enable_dpi_awareness_windows():
    if sys.platform != "win32":
        return
    try:
        import ctypes
        user32 = ctypes.windll.user32
        # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
        user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4))
    except Exception:
        try:
            import ctypes
            ctypes.windll.user32.SetProcessDPIAware()
        except Exception:
            pass


# ---- 設定 ----
APP_VERSION = "0.5.3"

DEFAULT_TESSERACT_PATH = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
OCR_INTERVAL_MS = 800
MAX_TEXT_LEN = 1500

OVERLAY_BG_ALPHA = 220

DEBUG_DIR = "debug"
DEBUG_LATEST_PATH = os.path.join(DEBUG_DIR, "latest.png")
DEBUG_LOG_PATH = os.path.join(DEBUG_DIR, "debug_log.txt")


# ---- EasyOCR(オプション)----
try:
    import easyocr  # type: ignore
    EASYOCR_AVAILABLE = True
except Exception:
    EASYOCR_AVAILABLE = False


@dataclass
class CaptureRect:
    left: int
    top: int
    width: int
    height: int

    def is_valid(self) -> bool:
        return self.width > 5 and self.height > 5


class RegionSelector(QtWidgets.QWidget):
    rectSelected = QtCore.Signal(QtCore.QRect)

    def __init__(self):
        super().__init__()
        self.setWindowTitle("Select Region")
        self.setWindowFlag(QtCore.Qt.WindowType.FramelessWindowHint, True)
        self.setWindowFlag(QtCore.Qt.WindowType.WindowStaysOnTopHint, True)
        self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True)
        self.setCursor(QtCore.Qt.CursorShape.CrossCursor)

        vg = QtGui.QGuiApplication.primaryScreen().virtualGeometry()
        self.setGeometry(vg)
        self.showFullScreen()

        self._origin_local: QtCore.QPoint | None = None
        self._current_local: QtCore.QPoint | None = None

    def paintEvent(self, event: QtGui.QPaintEvent) -> None:
        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
        painter.fillRect(self.rect(), QtGui.QColor(0, 0, 0, 60))

        if self._origin_local and self._current_local:
            r_local = QtCore.QRect(self._origin_local, self._current_local).normalized()
            painter.fillRect(r_local, QtGui.QColor(0, 0, 0, 0))

            pen = QtGui.QPen(QtGui.QColor(0, 180, 255, 220), 2)
            painter.setPen(pen)
            painter.drawRect(r_local)

            text = f"{r_local.width()} x {r_local.height()}"
            painter.setPen(QtGui.QColor(255, 255, 255, 240))
            painter.drawText(r_local.topLeft() + QtCore.QPoint(6, -6), text)

    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            self._origin_local = event.position().toPoint()
            self._current_local = self._origin_local
            self.update()

    def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
        if self._origin_local:
            self._current_local = event.position().toPoint()
            self.update()

    def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
        if event.button() == QtCore.Qt.MouseButton.LeftButton and self._origin_local:
            self._current_local = event.position().toPoint()
            r_local = QtCore.QRect(self._origin_local, self._current_local).normalized()

            tl_global = self.mapToGlobal(r_local.topLeft())
            br_global = self.mapToGlobal(r_local.bottomRight())
            r_global = QtCore.QRect(tl_global, br_global).normalized()

            self._origin_local = None
            self._current_local = None
            self.update()

            self.rectSelected.emit(r_global)
            self.close()

    def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
        if event.key() == QtCore.Qt.Key.Key_Escape:
            self.close()


def preprocess_pixel_pil(img: Image.Image, scale: int = 4, threshold: int = 185) -> Image.Image:
    g = img.convert("L")
    g = g.resize((g.width * scale, g.height * scale), resample=Image.Resampling.NEAREST)
    g = ImageOps.autocontrast(g)
    bw = g.point(lambda p: 255 if p > threshold else 0, mode="1")
    return bw.convert("L")


def tess_ocr(img: Image.Image, lang: str, psm: int, disable_dawg: bool = False, whitelist: str | None = None) -> str:
    cfg = [f"--oem 3 --psm {psm}"]
    if disable_dawg:
        cfg.append("-c load_system_dawg=0")
        cfg.append("-c load_freq_dawg=0")
    if whitelist:
        cfg.append(f"-c tessedit_char_whitelist={whitelist}")
    config = " ".join(cfg)
    return pytesseract.image_to_string(img, lang=lang, config=config)


class OverlayWindow(QtWidgets.QWidget):
    """
    - ドラッグ移動可能
    - 折りたたみ(本文だけ隠す)
    - 隠す(タスクバーから消す / show()で復帰)
    """
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Translation Overlay")

        # フレームレス + 常に最前面 + タスクバーに出さない(Tool)
        self.setWindowFlag(QtCore.Qt.WindowType.FramelessWindowHint, True)
        self.setWindowFlag(QtCore.Qt.WindowType.WindowStaysOnTopHint, True)
        self.setWindowFlag(QtCore.Qt.WindowType.Tool, True)

        self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True)

        self._dragging = False
        self._drag_offset = QtCore.QPoint(0, 0)
        self._collapsed = False
        self._expanded_size = QtCore.QSize(520, 240)

        # --- タイトルバー ---
        self.title = QtWidgets.QLabel("Translation")
        self.title.setStyleSheet("QLabel { color: white; font-weight: 600; }")

        self.btn_collapse = QtWidgets.QToolButton()
        self.btn_collapse.setText("_")  # 折りたたみ
        self.btn_collapse.setToolTip("折りたたみ / 展開")

        self.btn_hide = QtWidgets.QToolButton()
        self.btn_hide.setText("×")  # 隠す
        self.btn_hide.setToolTip("隠す(非表示)")

        self.btn_collapse.clicked.connect(self.toggle_collapse)
        self.btn_hide.clicked.connect(self.hide_overlay)

        bar = QtWidgets.QHBoxLayout()
        bar.setContentsMargins(10, 8, 10, 0)
        bar.addWidget(self.title)
        bar.addStretch(1)
        bar.addWidget(self.btn_collapse)
        bar.addWidget(self.btn_hide)

        # --- 本文 ---
        self._label = QtWidgets.QLabel("")
        self._label.setWordWrap(True)
        self._label.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextSelectableByMouse)

        # “背景の濃いパネル”を丸ごと一枚にして、バー+本文を載せる
        self.panel = QtWidgets.QFrame()
        self.panel.setStyleSheet(f"""
            QFrame {{
                background: rgba(0, 0, 0, {OVERLAY_BG_ALPHA});
                border: 1px solid rgba(255, 255, 255, 100);
                border-radius: 10px;
            }}
            QLabel {{
                background: transparent;
                color: white;
                font-size: 16px;
            }}
            QToolButton {{
                background: rgba(255,255,255,30);
                color: white;
                border: 1px solid rgba(255,255,255,60);
                border-radius: 6px;
                padding: 2px 6px;
            }}
            QToolButton:hover {{
                background: rgba(255,255,255,60);
            }}
        """)

        panel_layout = QtWidgets.QVBoxLayout(self.panel)
        panel_layout.setContentsMargins(0, 0, 0, 10)
        panel_layout.addLayout(bar)
        panel_layout.addWidget(self._label, 1)
        panel_layout.setSpacing(6)

        outer = QtWidgets.QVBoxLayout(self)
        outer.setContentsMargins(0, 0, 0, 0)
        outer.addWidget(self.panel)

        self.resize(self._expanded_size)
        self._label.setText("(翻訳結果がここに表示されます)")

    def set_text(self, text: str):
        if not self._collapsed:
            self._label.setText(text if text.strip() else "(文字が検出されません)")

    def set_text_cache(self, text: str):
        # 折りたたみ中でも内部更新しておく(展開したら見える)
        self._label.setText(text if text.strip() else "(文字が検出されません)")

    def toggle_collapse(self):
        if not self._collapsed:
            self._expanded_size = self.size()
            self._label.setVisible(False)
            self._collapsed = True
            self.btn_collapse.setText("")  # 展開アイコンっぽく
            # 高さを小さく
            self.resize(self._expanded_size.width(), 44)
        else:
            self._label.setVisible(True)
            self._collapsed = False
            self.btn_collapse.setText("_")
            self.resize(self._expanded_size)

    def hide_overlay(self):
        self.hide()

    # --- ドラッグ移動(どこ掴んでもOK)---
    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            self._dragging = True
            self._drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
            event.accept()

    def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
        if self._dragging:
            self.move(event.globalPosition().toPoint() - self._drag_offset)
            event.accept()

    def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            self._dragging = False
            event.accept()


class MainWindow(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(f"OCR → Translate Prototype v{APP_VERSION}")
        self.setMinimumWidth(820)

        self.tess_path = QtWidgets.QLineEdit(DEFAULT_TESSERACT_PATH)

        self.btn_pick = QtWidgets.QPushButton("処理範囲を選択(ドラッグ)")
        self.btn_start = QtWidgets.QPushButton("開始")
        self.btn_stop = QtWidgets.QPushButton("停止")
        self.btn_stop.setEnabled(False)

        # オーバーレイ表示/復帰
        self.btn_overlay_show = QtWidgets.QPushButton("翻訳ウィンドウを表示")
        self.btn_overlay_show.clicked.connect(self.show_overlay)

        self.chk_debug = QtWidgets.QCheckBox("デバッグ: debug/ にキャプチャ画像・ログを保存")
        self.chk_debug.setChecked(True)

        # 翻訳方向
        self.tr_mode = QtWidgets.QComboBox()
        self.tr_mode.addItem("English → Japanese", ("en", "ja"))
        self.tr_mode.addItem("Japanese → English", ("ja", "en"))

        # OCRモード
        self.ocr_mode = QtWidgets.QComboBox()
        self.ocr_mode.addItem("Tesseract: v0.5互換 (psm6)", "tess_psm6")
        self.ocr_mode.addItem("Tesseract: 字幕1行向け (psm7)", "tess_psm7")
        self.ocr_mode.addItem("Tesseract: ドット向けPIL前処理 + psm7 + 辞書OFF", "tess_pixel")
        if EASYOCR_AVAILABLE:
            self.ocr_mode.addItem("EasyOCR: フォント耐性強め(初回重い)", "easyocr")
        else:
            self.ocr_mode.addItem("EasyOCR: 未導入(無効)", "easyocr_unavailable")
            self.ocr_mode.model().item(self.ocr_mode.count() - 1).setEnabled(False)

        self.rect_label = QtWidgets.QLabel("未選択")
        self.status = QtWidgets.QLabel("待機中")
        self.raw_text = QtWidgets.QPlainTextEdit()
        self.raw_text.setReadOnly(True)

        form = QtWidgets.QFormLayout()
        form.addRow("tesseract.exe パス", self.tess_path)
        form.addRow("翻訳方向", self.tr_mode)
        form.addRow("OCRモード", self.ocr_mode)
        form.addRow("現在の範囲", self.rect_label)

        btn_row = QtWidgets.QHBoxLayout()
        btn_row.addWidget(self.btn_pick)
        btn_row.addStretch(1)
        btn_row.addWidget(self.btn_start)
        btn_row.addWidget(self.btn_stop)

        overlay_row = QtWidgets.QHBoxLayout()
        overlay_row.addWidget(self.btn_overlay_show)
        overlay_row.addStretch(1)

        layout = QtWidgets.QVBoxLayout(self)
        layout.addLayout(form)
        layout.addWidget(self.chk_debug)
        layout.addLayout(btn_row)
        layout.addLayout(overlay_row)
        layout.addWidget(QtWidgets.QLabel("ステータス"))
        layout.addWidget(self.status)
        layout.addWidget(QtWidgets.QLabel("OCR原文"))
        layout.addWidget(self.raw_text)

        self.overlay = OverlayWindow()
        self.overlay.show()  # 既定で表示

        self.timer = QtCore.QTimer(self)
        self.timer.setInterval(OCR_INTERVAL_MS)
        self.timer.timeout.connect(self.tick)

        self._busy = False
        self._last_ocr = ""
        self._last_translated = ""
        self._last_overlay_text = ""

        self.selector: RegionSelector | None = None

        self.selected_qt_global: QtCore.QRect | None = None
        self.mss_virtual = None  # dict

        self._easy_readers = {}

        self.btn_pick.clicked.connect(self.pick_region)
        self.btn_start.clicked.connect(self.start)
        self.btn_stop.clicked.connect(self.stop)

    def show_overlay(self):
        self.overlay.show()
        self.overlay.raise_()
        self.overlay.activateWindow()
        # 折りたたみ中でも表示内容を復元できるように
        self.overlay.set_text_cache(self._last_overlay_text or "(翻訳結果がここに表示されます)")

    def ensure_debug_dir(self):
        if self.chk_debug.isChecked():
            os.makedirs(DEBUG_DIR, exist_ok=True)

    def log_debug(self, msg: str):
        if not self.chk_debug.isChecked():
            return
        self.ensure_debug_dir()
        line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}\n"
        try:
            with open(DEBUG_LOG_PATH, "a", encoding="utf-8") as f:
                f.write(line)
        except Exception:
            pass

    def pick_region(self):
        self.status.setText("範囲選択中(ドラッグ / ESCでキャンセル)")
        self.selector = RegionSelector()
        self.selector.rectSelected.connect(self.on_region_selected)
        self.selector.destroyed.connect(lambda: setattr(self, "selector", None))
        self.selector.show()

    def on_region_selected(self, r_qt_global: QtCore.QRect):
        self.selected_qt_global = r_qt_global
        qt_vg = QtGui.QGuiApplication.primaryScreen().virtualGeometry()

        self.rect_label.setText(
            f"QtRect=({r_qt_global.left()},{r_qt_global.top()},{r_qt_global.width()},{r_qt_global.height()})"
        )
        self.status.setText("範囲が選択されました。開始できます。")

        self.log_debug(
            f"Qt virtualGeometry: left={qt_vg.left()} top={qt_vg.top()} w={qt_vg.width()} h={qt_vg.height()}"
        )

        # 初回だけ “近くに置く” くらい(以後はユーザーが動かす前提)
        if not self.overlay.isVisible():
            self.overlay.move(r_qt_global.right() + 12, r_qt_global.top())
            self.overlay.show()

    def start(self):
        tp = self.tess_path.text().strip()
        if tp and os.path.exists(tp):
            pytesseract.pytesseract.tesseract_cmd = tp
        else:
            self.status.setText("tesseract.exe のパスが見つかりません。")
            return

        if not self.selected_qt_global or self.selected_qt_global.width() < 6 or self.selected_qt_global.height() < 6:
            self.status.setText("処理範囲が未選択です。")
            return

        with mss.mss() as sct:
            self.mss_virtual = sct.monitors[0]
            self.log_debug(f"mss monitors[0] (virtual): {self.mss_virtual}")

        self.ensure_debug_dir()
        self._last_ocr = ""
        self._last_translated = ""

        self.btn_start.setEnabled(False)
        self.btn_stop.setEnabled(True)
        self.btn_pick.setEnabled(False)

        src, tgt = self.tr_mode.currentData()
        mode = self.ocr_mode.currentData()
        self.status.setText(f"実行中…(OCR: {mode}, 翻訳: {src}->{tgt})")
        self.timer.start()

    def stop(self):
        self.timer.stop()
        self.btn_start.setEnabled(True)
        self.btn_stop.setEnabled(False)
        self.btn_pick.setEnabled(True)
        self.status.setText("停止しました。")

    def qt_rect_to_mss_rect(self, r_qt_global: QtCore.QRect) -> CaptureRect:
        assert self.mss_virtual is not None
        qt_vg = QtGui.QGuiApplication.primaryScreen().virtualGeometry()
        mv = self.mss_virtual

        scale_x = mv["width"] / qt_vg.width()
        scale_y = mv["height"] / qt_vg.height()

        left = int((r_qt_global.left() - qt_vg.left()) * scale_x + mv["left"])
        top = int((r_qt_global.top() - qt_vg.top()) * scale_y + mv["top"])
        width = int(r_qt_global.width() * scale_x)
        height = int(r_qt_global.height() * scale_y)

        return CaptureRect(left, top, width, height)

    def get_translator(self):
        src, tgt = self.tr_mode.currentData()
        return GoogleTranslator(source=src, target=tgt)

    def get_easy_reader(self, src_lang: str):
        if src_lang not in self._easy_readers:
            self.status.setText("EasyOCR初期化中(初回だけ少し時間がかかります)")
            QtWidgets.QApplication.processEvents()
            self._easy_readers[src_lang] = easyocr.Reader([src_lang], gpu=False)
        return self._easy_readers[src_lang]

    def tick(self):
        if self._busy:
            return
        self._busy = True
        try:
            if not self.selected_qt_global or not self.mss_virtual:
                return

            cap = self.qt_rect_to_mss_rect(self.selected_qt_global)
            if not cap.is_valid():
                return

            img = self.capture_region(cap)

            if self.chk_debug.isChecked():
                self.ensure_debug_dir()
                try:
                    img.save(DEBUG_LATEST_PATH)
                except Exception:
                    pass

            src, tgt = self.tr_mode.currentData()
            mode = self.ocr_mode.currentData()

            tess_lang = "eng" if src == "en" else "jpn"

            if mode == "tess_psm6":
                text = tess_ocr(img.convert("L"), lang=tess_lang, psm=6)

            elif mode == "tess_psm7":
                text = tess_ocr(img.convert("L"), lang=tess_lang, psm=7)

            elif mode == "tess_pixel":
                pre = preprocess_pixel_pil(img, scale=4, threshold=185)
                if src == "en":
                    whitelist = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?;:'\"-()[] "
                    text = tess_ocr(pre, lang="eng", psm=7, disable_dawg=True, whitelist=whitelist)
                else:
                    text = tess_ocr(pre, lang="jpn", psm=7, disable_dawg=True)

            elif mode == "easyocr":
                reader = self.get_easy_reader(src)
                import numpy as _np
                arr = _np.array(img.convert("RGB"))
                results = reader.readtext(arr, detail=0, paragraph=True)
                text = "\n".join(results)

            else:
                text = ""

            text = (text or "").strip()
            text = text[:MAX_TEXT_LEN].strip()

            if text and text != self._last_ocr:
                self._last_ocr = text
                self.raw_text.setPlainText(text)

                try:
                    translated = self.get_translator().translate(text)
                except Exception as e:
                    translated = f"翻訳エラー: {e}"

                if translated != self._last_translated:
                    self._last_translated = translated
                    self._last_overlay_text = translated
                    # 折りたたみ中も内部更新
                    self.overlay.set_text_cache(translated)
                    self.overlay.set_text(translated)

            elif not text:
                self.raw_text.setPlainText("")
                self._last_overlay_text = "(文字が検出されません)"
                self.overlay.set_text_cache(self._last_overlay_text)
                self.overlay.set_text(self._last_overlay_text)

        finally:
            self._busy = False

    @staticmethod
    def capture_region(rect: CaptureRect) -> Image.Image:
        with mss.mss() as sct:
            monitor = {"left": rect.left, "top": rect.top, "width": rect.width, "height": rect.height}
            shot = sct.grab(monitor)
            return Image.frombytes("RGB", shot.size, shot.rgb)


def main():
    enable_dpi_awareness_windows()
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

ダウンロード

レンタルサーバだとウイルス検出等不安があるため、Googleドライブで配布します。
次のリンク先からzipをダウンロードし、zip回答して app.exeを起動してください。
ocr_translate_overlay
https://drive.google.com/drive/folders/1OUL66DKSsvZVrW8y-2aVFLTl2zpiQUjf?usp=drive_link

追加インストールが必要なTesseract OCR について
本ツールで Tesseract OCR モードを使用する場合は、
別途 Tesseract OCR をインストールしてください。
公式サイト:
https://github.com/tesseract-ocr/tesseract
インストール後、アプリ内の
「tesseract.exe パス」に実行ファイルの場所を指定してください。

使い方

OCR Translate Overlay v0.5.3
画面OCR+翻訳 オーバーレイツール(Windows)

翻訳中、少し画面操作の反応が悪くなります。その時は、停止ボタンを押してください。リアルタイム翻訳が停止します。

■ このツールについて

本ツールは、画面上の文字(英語・日本語)をOCRで読み取り、
翻訳結果をデスクトップ上にフローティング表示する
Windows向けのオーバーレイ翻訳アプリです。

・ゲーム画面の字幕
・海外アプリや英語UI
・動画再生中のテキスト

などを、アプリを切り替えずに翻訳できます。

Pythonや仮想環境の構築は不要です。
exeを起動するだけで利用できます。

■ 動作環境

・Windows 10 / Windows 11(64bit)
・インターネット接続(翻訳に使用)

※ EasyOCRモードを使用する場合、初回起動時は
 少し時間がかかることがあります。

■ フォルダ構成(重要)

以下の構成を保ったまま使用してください。

OCRTranslateOverlay/
├─ app.exe
├─ debug(debug機能をオンにしたときにスキャン範囲を画像化します、スキャン座標確認用)
├─ LICENSE.txt
└─ readme.txt

※ フォルダごと移動しても動作します
※ Program Files 配下でも、ユーザーフォルダでも使用可能です

■ 起動方法

  1. app.exe をダブルクリックします
  2. メインウィンドウと翻訳オーバーレイが表示されます

※ 初回起動時、Windowsの警告が出る場合がありますが
 「詳細情報」→「実行」で起動できます

■ 基本的な使い方

  1. 「処理範囲を選択(ドラッグ)」をクリック
  2. 翻訳したい画面の文字部分をドラッグで囲みます
  3. 「開始」をクリック
  4. 翻訳結果が画面上のオーバーレイに表示されます

停止したい場合は「停止」をクリックしてください。

■ 翻訳方向の切り替え

画面上部の「翻訳方向」プルダウンから選択できます。

・English → Japanese(英語 → 日本語)
・Japanese → English(日本語 → 英語)

※ 翻訳方向に応じてOCR言語も自動で切り替わります

■ OCRモードについて

用途に応じてOCR方式を切り替えできます。

・Tesseract (psm6)
標準的なOCR。安定重視。

・Tesseract (psm7)
1行字幕向け。ゲーム字幕に向いています。

・Tesseract(ドット文字向け)
ドットフォントや荒い文字向け。

・EasyOCR(高精度)
デザインフォントやドット文字に強い。
初回起動時は少し時間がかかります。

※ 認識精度は画面やフォントによって異なります。
 まず EasyOCR を試すのがおすすめです。

■ 翻訳オーバーレイの操作

翻訳結果が表示されるウィンドウは以下の操作が可能です。

・ドラッグ:ウィンドウを自由に移動
・「_」ボタン:折りたたみ(本文を一時的に非表示)
・「×」ボタン:オーバーレイを非表示

非表示にした場合は、
メインウィンドウの「翻訳ウィンドウを表示」で再表示できます。

■ デバッグ機能

「デバッグ」にチェックを入れると、
以下の情報が debug フォルダに保存されます。

・OCR対象のキャプチャ画像
・動作ログ(debug_log.txt)

OCR精度の確認やトラブルシュートに使用できます。

■ 注意事項

・本ツールはオンライン翻訳を使用します
・翻訳結果の正確性は保証されません
・ゲームやアプリの利用規約に注意してください
・本ツールの使用による不具合・損害について
作者は責任を負いません

■ クレジット

・Tesseract OCR
・EasyOCR
・PySide6 (Qt)
・deep-translator

本ツールは個人制作の実験的ツールです。

■ 作者・配布元

IT Libero
https://it-libero.com/
License: MIT