Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 43 additions & 40 deletions addon/globalPlugins/ERE/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@
import globalPluginHandler
import globalVars
import threading
import time
import wx
import speech
import speechDictHandler
from copy import deepcopy
from logHandler import log
from .constants import *
from . import updater
from . import compatibilityUtil
from ._englishToKanaConverter.englishToKanaConverter import EnglishToKanaConverter
from . import extensionPoints
from . import speechHook
from .textProcessors import getEnglishToKanaProcessor
from scriptHandler import script

try:
Expand All @@ -42,60 +40,64 @@ def __init__(self):
self.autoUpdateChecker = updater.AutoUpdateChecker()
self.autoUpdateChecker.autoUpdateCheck()
self._setupMenu()
self._initializeExtensionPoints()
if self.getStateSetting():
self._setup()
t = threading.Thread(target=self._checkAutoLanguageSwitchingState, daemon=True)
t.start()

def _initializeExtensionPoints(self):
"""拡張ポイントを初期化し、ハンドラを登録する。"""
# デフォルトの辞書パターン削除を登録
speechHook.registerDefaultPatternRemovals()

# EnglishToKanaプロセッサを前処理拡張ポイントに登録
# これによりNVDAの標準処理の前にテキストが変換される
self._textProcessor = getEnglishToKanaProcessor()
extensionPoints.textProcessing.preProcessText.register(self._textProcessor)

def _checkAutoLanguageSwitchingState(self):
if self.getStateSetting() and config.conf["speech"]["autoLanguageSwitching"]:
compatibilityUtil.messageBox(_("Automatic Language switching is enabled. English Reading Enhancer may not work correctly. To use this add-on, we recommend to disable this functionality."), _("Warning"))

def terminate(self):
super(GlobalPlugin, self).terminate()
self._fullUnsetup()
# 拡張ポイントハンドラの登録を解除
extensionPoints.textProcessing.preProcessText.unregister(self._textProcessor)
try:
gui.mainFrame.sysTrayIcon.menu.Remove(self.rootMenuItem)
except BaseException:
pass

def _setup(self):
if hasattr(speech, "speech"):
self.processText_original = speech.speech.processText
else:
self.processText_original = speech.processText
c = EnglishToKanaConverter()

def processText(locale, text, symbolLevel, **kwargs):
# 2026/01/11 本家のprocessTextよりも前にカナ変換をするように変更
# 従来の実装ではアポストロフィーなどの記号が読みに変換されたあとで処理されるため、「haven't」などが正しく読めなかった
if locale.startswith("ja") and self.getStateSetting():
text = c.process(text)
text = self.processText_original(locale, text, symbolLevel, **kwargs)
return text
if hasattr(speech, "speech"):
speech.speech.processText = processText
else:
speech.processText = processText
# modify builtin speech dicts
unusedEntries = []
unusedPatterns = (
"([a-z])([A-Z])",
"([A-Z])([A-Z][a-z])",
)
for entry in speechDictHandler.dictionaries["builtin"]:
if entry.pattern in unusedPatterns:
unusedEntries.append(entry)
self.builtinDict_original = deepcopy(speechDictHandler.dictionaries["builtin"])
for entry in unusedEntries:
index = speechDictHandler.dictionaries["builtin"].index(entry)
del speechDictHandler.dictionaries["builtin"][index]
"""拡張ポイントを使用してテキスト処理を有効にする。"""
manager = speechHook.getManager()

# まだインストールされていない場合は音声フックをインストール
if not manager.isInstalled:
manager.install()
manager.applyDictionaryModifications()

# テキスト処理を有効にする
manager.setEnabled(True)

def _unsetup(self):
if hasattr(speech, "speech"):
speech.speech.processText = self.processText_original
else:
speech.processText = self.processText_original
speechDictHandler.dictionaries["builtin"] = self.builtinDict_original
"""テキスト処理を無効にし、元の状態を復元する。"""
manager = speechHook.getManager()

# テキスト処理を無効にする
manager.setEnabled(False)

# 完全にシャットダウンする場合のみアンインストールと復元を行う
# (これはterminateで処理される)

def _fullUnsetup(self):
"""音声フックを完全にアンインストールする(terminate時に使用)。"""
manager = speechHook.getManager()
manager.setEnabled(False)
manager.restoreDictionaryModifications()
manager.uninstall()

def _setupMenu(self):
self.rootMenu = wx.Menu()
Expand Down Expand Up @@ -167,6 +169,7 @@ def reportMisreadings(self, evt):
compatibilityUtil.messageBox(_("Before using this feature, please set your GitHub Access Token."), _("Error"))
return
from .dialogs import reportMisreadingsDialog
from ._englishToKanaConverter.englishToKanaConverter import EnglishToKanaConverter
gui.mainFrame.prePopup()
dialog = reportMisreadingsDialog.ReportMisreadingsDialog(gui.mainFrame)
res = gui.message.displayDialogAsModal(dialog)
Expand Down
171 changes: 171 additions & 0 deletions addon/globalPlugins/ERE/extensionPoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# coding: UTF-8
"""
English Reading Enhancer用の拡張ポイントモジュール

このモジュールは、関数オーバーライド/モンキーパッチの代わりに、
ハンドラを登録して呼び出すためのクリーンで疎結合な
テキスト処理用の拡張ポイント基盤を提供します。

拡張ポイントの種類:
- Filter: ハンドラがデータを変更できるようにする
- Action: 何かが起きた時にハンドラに通知する(戻り値なし)
"""

from __future__ import unicode_literals
from typing import Callable, List, Any, Optional
from logHandler import log


class HandlerRegistrar:
"""ハンドラ登録を管理する基底クラス。"""

def __init__(self):
self._handlers: List[Callable] = []

def register(self, handler: Callable) -> None:
"""ハンドラ関数を登録する。"""
if handler not in self._handlers:
self._handlers.append(handler)
log.debug(f"Handler registered: {handler.__name__ if hasattr(handler, '__name__') else handler}")

def unregister(self, handler: Callable) -> None:
"""ハンドラ関数の登録を解除する。"""
if handler in self._handlers:
self._handlers.remove(handler)
log.debug(f"Handler unregistered: {handler.__name__ if hasattr(handler, '__name__') else handler}")

def isRegistered(self, handler: Callable) -> bool:
"""ハンドラが登録されているか確認する。"""
return handler in self._handlers

@property
def handlers(self) -> List[Callable]:
"""登録されているハンドラのリスト(コピー)を取得する。"""
return self._handlers.copy()


class Filter(HandlerRegistrar):
"""
ハンドラがデータをフィルタ/変更できる拡張ポイント。

ハンドラは登録順に呼び出され、それぞれが前のハンドラの出力を受け取る。
これにより処理パイプラインが作成される。

使用例:
textFilter = Filter()
textFilter.register(my_handler)
result = textFilter.apply(initial_value, locale="ja")
"""

def apply(self, value: Any, **kwargs) -> Any:
"""
登録されているすべてのハンドラを値に適用する。

引数:
value: フィルタリングする初期値
**kwargs: すべてのハンドラに渡される追加のキーワード引数

戻り値:
すべてのハンドラが処理した後のフィルタリングされた値
"""
for handler in self._handlers:
try:
value = handler(value, **kwargs)
except Exception as e:
log.error(f"Error in filter handler {handler}: {e}")
return value


class Action(HandlerRegistrar):
"""
イベント発生時にハンドラに通知する拡張ポイント。

Filterとは異なり、ハンドラは値を返さない。
何かが起きたという通知を受け取るだけ。

使用例:
onEnabled = Action()
onEnabled.register(my_handler)
onEnabled.notify(some_data="value")
"""

def notify(self, **kwargs) -> None:
"""
登録されているすべてのハンドラに通知する。

引数:
**kwargs: すべてのハンドラに渡されるキーワード引数
"""
for handler in self._handlers:
try:
handler(**kwargs)
except Exception as e:
log.error(f"Error in action handler {handler}: {e}")


class TextProcessingExtensionPoint:
"""
音声合成前のテキスト処理用の拡張ポイント。

以前のモンキーパッチ方式を、テキストプロセッサを登録して
順番に適用できるクリーンな拡張ポイントに置き換える。
"""

# NVDAのprocessTextの前に適用されるフィルタ
preProcessText = Filter()

# NVDAのprocessTextの後に適用されるフィルタ
postProcessText = Filter()

# 処理が有効になった時にトリガーされるアクション
onEnabled = Action()

# 処理が無効になった時にトリガーされるアクション
onDisabled = Action()


class SpeechDictModifier:
"""
音声辞書を変更するための拡張ポイント。

直接操作せずに適用/元に戻すことができる
辞書変更を登録するためのクリーンなインターフェースを提供する。
"""

def __init__(self):
self._modifications: List[dict] = []
self._originalState: Optional[Any] = None

def registerPatternRemoval(self, pattern: str) -> None:
"""組み込み辞書から削除するパターンを登録する。"""
self._modifications.append({
'type': 'remove_pattern',
'pattern': pattern
})

def getModifications(self) -> List[dict]:
"""登録されている変更のリストを取得する。"""
return self._modifications.copy()

def setOriginalState(self, state: Any) -> None:
"""復元用に元の辞書状態を保存する。"""
self._originalState = state

def getOriginalState(self) -> Optional[Any]:
"""保存されている元の状態を取得する。"""
return self._originalState


# グローバル拡張ポイントインスタンス
textProcessing = TextProcessingExtensionPoint()
speechDictModifier = SpeechDictModifier()


def resetAll() -> None:
"""すべての拡張ポイントをリセットする(テストやクリーンアップに便利)。"""
textProcessing.preProcessText._handlers.clear()
textProcessing.postProcessText._handlers.clear()
textProcessing.onEnabled._handlers.clear()
textProcessing.onDisabled._handlers.clear()
speechDictModifier._modifications.clear()
speechDictModifier._originalState = None
Loading